Stuff
This commit is contained in:
parent
bb6d7346f7
commit
88f827d4b4
82
costly-api/package-lock.json
generated
82
costly-api/package-lock.json
generated
@ -16,6 +16,7 @@
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"express-session": "^1.18.2",
|
||||
"mysql2": "^3.16.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
@ -31,6 +32,7 @@
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.10.7",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
@ -3362,6 +3364,16 @@
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-session": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz",
|
||||
"integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||
@ -6368,6 +6380,46 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
|
||||
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.7",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-headers": "~1.1.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "~2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express-session/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express/node_modules/content-disposition": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||
@ -9072,6 +9124,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@ -9626,6 +9687,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@ -11456,6 +11526,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"random-bytes": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/uint8array-extras": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"express-session": "^1.18.2",
|
||||
"mysql2": "^3.16.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
@ -42,6 +43,7 @@
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.10.7",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
|
||||
@ -6,16 +6,31 @@ import { DatabaseModule } from './database/database.module';
|
||||
import { GroupsModule } from './groups/groups.module';
|
||||
import { ExpensesModule } from './expenses/expenses.module';
|
||||
import { GroupEventsModule } from './group-events/group-events.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule.forRoot({
|
||||
envFilePath: '.env',
|
||||
isGlobal: true
|
||||
}),
|
||||
DatabaseModule,
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'mysql',
|
||||
host: configService.get('DATABASE_HOST') || 'localhost',
|
||||
port: parseInt(configService.get('DATABASE_PORT') || '0'),
|
||||
username: configService.get('DATABASE_USER'),
|
||||
password: configService.get('DATABASE_PASSWORD'),
|
||||
database: configService.get('DATABASE_NAME'),
|
||||
entities: [],
|
||||
synchronize: (configService.get('DATABASE_SYNC')||'').toLowerCase() == 'true',
|
||||
})
|
||||
}),
|
||||
GroupsModule,
|
||||
ExpensesModule,
|
||||
GroupEventsModule
|
||||
GroupEventsModule,
|
||||
DatabaseModule
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
@ -6,26 +6,14 @@ import { GroupEventEntity } from 'src/groups/persistence/group-event.entity';
|
||||
import { ExpenseEntity } from 'src/expenses/persistence/expense.entity';
|
||||
import { ExpenseEventEntity } from 'src/expenses/persistence/expense-event.entity';
|
||||
import { GroupMemberEntity } from 'src/groups/persistence/group-member.entity';
|
||||
import { ExpenseSplitEntity } from 'src/expenses/persistence/expense-split.entity';
|
||||
|
||||
const ENTITY = [GroupEntity, GroupEventEntity, ExpenseEntity, ExpenseEventEntity, GroupMemberEntity]
|
||||
const ENTITY = [GroupEntity, GroupEventEntity, ExpenseEntity, ExpenseEventEntity, GroupMemberEntity, ExpenseSplitEntity]
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'mysql',
|
||||
host: configService.get('DATABASE_HOST') || 'localhost',
|
||||
port: parseInt(configService.get('DATABASE_PORT') || '0'),
|
||||
username: configService.get('DATABASE_USER'),
|
||||
password: configService.get('DATABASE_PASSWORD'),
|
||||
database: configService.get('DATABASE_NAME'),
|
||||
entities: ENTITY,
|
||||
synchronize: (configService.get('DATABASE_SYNC')||'').toLowerCase() == 'true',
|
||||
})
|
||||
})
|
||||
TypeOrmModule.forFeature(ENTITY)
|
||||
],
|
||||
providers: [ConfigService,],
|
||||
exports: [TypeOrmModule]
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export class CreateExpenseDto {
|
||||
name: string;
|
||||
amount: number;
|
||||
groupId: number;
|
||||
groupId: string;
|
||||
}
|
||||
@ -3,14 +3,10 @@ import { DatabaseModule } from 'src/database/database.module';
|
||||
import { ExpensesService } from './application/expenses/expenses.service';
|
||||
import { ExpensesController } from './api/expenses/expenses.controller';
|
||||
import { ExpensesRepository } from './persistence/expense.repository';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ExpenseEntity } from './persistence/expense.entity';
|
||||
import { ExpenseEventEntity } from './persistence/expense-event.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
TypeOrmModule.forFeature([ExpenseEntity, ExpenseEventEntity])
|
||||
DatabaseModule
|
||||
],
|
||||
providers: [ExpensesService, ExpensesRepository],
|
||||
controllers: [ExpensesController]
|
||||
|
||||
19
costly-api/src/expenses/persistence/expense-split.entity.ts
Normal file
19
costly-api/src/expenses/persistence/expense-split.entity.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Column, Entity, ManyToOne, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { ExpenseEntity } from "./expense.entity";
|
||||
import { GroupMemberEntity } from "src/groups/persistence/group-member.entity";
|
||||
|
||||
@Entity('expense_splits')
|
||||
export class ExpenseSplitEntity {
|
||||
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ManyToOne(expense => ExpenseEntity, expense => expense.splits)
|
||||
expense: ExpenseEntity;
|
||||
|
||||
@Column({ type: 'float'})
|
||||
weight: number;
|
||||
|
||||
@ManyToOne(member => GroupMemberEntity, member => member.splits, { eager: true })
|
||||
member: GroupMemberEntity;
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { GroupEntity } from "src/groups/persistence/group.entity";
|
||||
import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
|
||||
import { Column, CreateDateColumn, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
|
||||
import { ExpenseSplitEntity } from "./expense-split.entity";
|
||||
|
||||
@Entity('expenses')
|
||||
export class ExpenseEntity {
|
||||
@ -10,9 +11,12 @@ export class ExpenseEntity {
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@OneToMany(type => GroupEntity, group => group.expenses)
|
||||
@ManyToOne(type => GroupEntity, group => group.expenses)
|
||||
group: GroupEntity;
|
||||
|
||||
@OneToMany(type => ExpenseSplitEntity, split => split.expense, { eager: true, cascade: true})
|
||||
splits: ExpenseSplitEntity[];
|
||||
|
||||
@Column()
|
||||
amount: number;
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post, Req, Session } from '@nestjs/common';
|
||||
import { GroupEventsService } from '../application/group-events.service';
|
||||
import { IncomingGroupEventDto } from '../dto/group-event.dto';
|
||||
|
||||
@ -17,13 +17,13 @@ export class GroupEventsController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
getEvents() {
|
||||
return this.groupsEventsService.getLastTenEvents();
|
||||
getEvents(@Session() session: Record<string, any>) {
|
||||
return this.groupsEventsService.getLastEvents({});
|
||||
}
|
||||
|
||||
@Get(':groupdId')
|
||||
getGroupEvents(@Param('groupId') groupId: string) {
|
||||
return this.groupsEventsService.getLastTenEvents(groupId);
|
||||
return this.groupsEventsService.getLastEvents({groupId});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -9,8 +9,9 @@ import { ExpenseEntity } from 'src/expenses/persistence/expense.entity';
|
||||
import { GroupsRepository } from 'src/groups/persistence/group.repository';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { IncomingEventDto } from 'src/model/dto/incoming-event.dto';
|
||||
import { IncomingGroupEventDto } from '../dto/group-event.dto';
|
||||
import { GroupEventType, GROUPEVENTTYPE, GroupRenamedPayload, IncomingGroupEventDto, TypedIncomingGroupEvent } from '../dto/group-event.dto';
|
||||
import { GroupMemberEntity } from 'src/groups/persistence/group-member.entity';
|
||||
import { ExpenseSplitEntity } from 'src/expenses/persistence/expense-split.entity';
|
||||
|
||||
@Injectable()
|
||||
export class GroupEventsService {
|
||||
@ -57,13 +58,15 @@ export class GroupEventsService {
|
||||
// optional: nur diejenigen anwenden, die "gerade neu" sind
|
||||
// (Wenn du ganz sauber sein willst: execute() liefert affected, aber nicht IDs.
|
||||
// Pragmatik: apply ist idempotent, dann ist doppelt anwenden ok, aber nur wenn Operationen idempotent sind!)
|
||||
for (const ev of persisted) {
|
||||
for (const ev of (persisted as TypedIncomingGroupEvent[])) {
|
||||
if (ev.type === 'GROUP_RENAMED') {
|
||||
await this.ingestGroupRenameEvent(groupId, ev)
|
||||
await this.ingestGroupRenameEvent(groupId, ev as any)
|
||||
} else if (ev.type == 'MEMBER_CREATED') {
|
||||
await this.ingestGroupAddMemberEvent(groupId, ev);
|
||||
await this.ingestGroupAddMemberEvent(groupId, ev as any);
|
||||
} else if (ev.type == 'MEMBER_RENAMED') {
|
||||
await this.ingestMemberRenamedEvent(groupId, ev);
|
||||
await this.ingestMemberRenamedEvent(groupId, ev as any);
|
||||
} else if (ev.type == 'EXPENSE_CREATED') {
|
||||
await this.ingestCreateExpense(groupId, ev as any)
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,11 +74,11 @@ export class GroupEventsService {
|
||||
});
|
||||
}
|
||||
|
||||
async ingestMemberRenamedEvent(groupId: string, ev: GroupEventEntity) {
|
||||
async ingestMemberRenamedEvent(groupId: string, ev: TypedIncomingGroupEvent<'MEMBER_RENAMED'>) {
|
||||
if (!ev.payload.to) { throw new BadRequestException('to is required'); }
|
||||
if (!ev.payload.from) { throw new BadRequestException('from is required'); }
|
||||
try {
|
||||
const member = await this.groupMembers.findOneByOrFail({ id: ev.payload.id + '', group: { id: groupId }});
|
||||
const member = await this.groupMembers.findOneByOrFail({ id: ev.payload.memberId + '', group: { id: groupId }});
|
||||
member.name = ev.payload.to;
|
||||
await this.groupMembers.save(member);
|
||||
return member;
|
||||
@ -84,7 +87,7 @@ export class GroupEventsService {
|
||||
}
|
||||
}
|
||||
|
||||
async ingestGroupAddMemberEvent(groupId: string, ev: GroupEventEntity) {
|
||||
async ingestGroupAddMemberEvent(groupId: string, ev: TypedIncomingGroupEvent<'MEMBER_CREATED'>) {
|
||||
const group = await this.groupsRepo.findOneByOrFail({ id: groupId });
|
||||
const member = await this.groupMembers.save(this.groupMembers.create({
|
||||
name: ev.payload.name,
|
||||
@ -92,20 +95,43 @@ export class GroupEventsService {
|
||||
}))
|
||||
return member;
|
||||
}
|
||||
async ingestGroupRenameEvent(groupId: string, ev: GroupEventEntity) {
|
||||
async ingestGroupRenameEvent(groupId: string, ev: TypedIncomingGroupEvent<'GROUP_RENAMED'>) {
|
||||
const group = await this.groupsRepo.findOneByOrFail({ id: groupId });
|
||||
group.name = ev.payload.to;
|
||||
const saved = await this.groupsRepo.save(group);
|
||||
return saved;
|
||||
}
|
||||
|
||||
getLastTenEvents(groupId?: string) {
|
||||
getLastEvents({groupId, amount}: {groupId?: string, amount?: 10}) {
|
||||
const rep = this.dataSource.getRepository(GroupEventEntity);
|
||||
return rep.find({
|
||||
where: { groupId },
|
||||
order: { createdAt: 'DESC' },
|
||||
take: 10
|
||||
take: amount
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
async ingestCreateExpense(groupId: string, ev: GroupEventEntity) {
|
||||
const group = await this.groupsRepo.findOneByOrFail({ id: groupId });
|
||||
const repo = this.dataSource.getRepository(ExpenseEntity);
|
||||
const splitRepo = this.dataSource.getRepository(ExpenseSplitEntity);
|
||||
const dto = ev.payload as CreateExpenseDto;
|
||||
dto.groupId = groupId;
|
||||
const exp = repo.create(dto);
|
||||
exp.group = group;
|
||||
|
||||
const expense = await repo.save(exp)
|
||||
for (let m of group.members) {
|
||||
const s = await splitRepo.save(
|
||||
splitRepo.create({
|
||||
expense,
|
||||
member: m,
|
||||
weight: 1 / group.members.length
|
||||
})
|
||||
)
|
||||
console.log(s)
|
||||
}
|
||||
return exp;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,4 +12,56 @@ export class IncomingGroupEventDto {
|
||||
|
||||
@IsOptional() @IsISO8601()
|
||||
clientCreatedAt?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export type GroupEventType =
|
||||
| 'GROUP_RENAMED'
|
||||
| 'EXPENSE_CREATED'
|
||||
| 'EXPENSE_AMOUNT_MODIFIED'
|
||||
| 'MEMBER_CREATED'
|
||||
| 'MEMBER_RENAMED';
|
||||
|
||||
export interface GroupRenamedPayload {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface ExpenseCreatedPayload {
|
||||
expenseId: string;
|
||||
payerMemberId: string;
|
||||
amountCents: number;
|
||||
splits: Array<{ memberId: string; weight: number }>;
|
||||
}
|
||||
|
||||
export interface ExpenseAmountModifiedPayload {
|
||||
expenseId: string;
|
||||
fromAmountCents: number;
|
||||
toAmountCents: number;
|
||||
}
|
||||
|
||||
export interface MemberCreatedPayload {
|
||||
memberId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface MemberRenamedPayload {
|
||||
memberId: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export type GroupEventPayloadByType = {
|
||||
GROUP_RENAMED: GroupRenamedPayload;
|
||||
EXPENSE_CREATED: ExpenseCreatedPayload;
|
||||
EXPENSE_AMOUNT_MODIFIED: ExpenseAmountModifiedPayload;
|
||||
MEMBER_CREATED: MemberCreatedPayload;
|
||||
MEMBER_RENAMED: MemberRenamedPayload;
|
||||
};
|
||||
|
||||
export type TypedIncomingGroupEvent<T extends GroupEventType = GroupEventType> = {
|
||||
id: string;
|
||||
type: T;
|
||||
payload: GroupEventPayloadByType[T];
|
||||
actorId?: string;
|
||||
clientCreatedAt?: string;
|
||||
};
|
||||
|
||||
@ -9,6 +9,6 @@ import { GroupMemberEntity } from 'src/groups/persistence/group-member.entity';
|
||||
@Module({
|
||||
controllers: [GroupEventsController],
|
||||
providers: [GroupEventsService],
|
||||
imports: [DatabaseModule, TypeOrmModule.forFeature([GroupEventsController, GroupEntity, GroupMemberEntity])]
|
||||
imports: [DatabaseModule]
|
||||
})
|
||||
export class GroupEventsModule {}
|
||||
|
||||
@ -5,9 +5,10 @@ import { GroupsController } from './api/groups.controller';
|
||||
import { GroupsService } from './application/groups.service';
|
||||
import { GroupsRepository } from './persistence/group.repository';
|
||||
import { GroupEventEntity } from './persistence/group-event.entity';
|
||||
import { DatabaseModule } from 'src/database/database.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([GroupEntity, GroupEventEntity])],
|
||||
imports: [DatabaseModule],
|
||||
controllers: [GroupsController],
|
||||
providers: [GroupsService, GroupsRepository]
|
||||
})
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Column, CreateDateColumn, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
|
||||
import { GroupEntity } from "./group.entity";
|
||||
import { ExpenseSplitEntity } from "src/expenses/persistence/expense-split.entity";
|
||||
|
||||
@Entity('group_members')
|
||||
export class GroupMemberEntity {
|
||||
@ -10,9 +11,14 @@ export class GroupMemberEntity {
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
balance: number = 0;
|
||||
|
||||
@ManyToOne(type => GroupEntity, group => group.members)
|
||||
group: GroupEntity;
|
||||
|
||||
@OneToMany(type => ExpenseSplitEntity, split => split.member)
|
||||
splits: ExpenseSplitEntity[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
@ -17,7 +17,8 @@ export class GroupEntity {
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
expenses: ExpenseEntity[] = [];
|
||||
@OneToMany(type => ExpenseEntity, expense => expense.group, { eager: true })
|
||||
expenses: ExpenseEntity[];
|
||||
|
||||
@OneToMany(type => GroupMemberEntity, member => member.group, { eager: true })
|
||||
members: GroupMemberEntity[];
|
||||
|
||||
@ -12,14 +12,24 @@ export class GroupsRepository {
|
||||
private readonly repo: Repository<GroupEntity>,
|
||||
) {}
|
||||
|
||||
findById(id: string) {
|
||||
return this.repo.findOne({ where: { id } });
|
||||
async findById(id: string) {
|
||||
const group = await this.repo.findOneOrFail({ where: { id } });
|
||||
this.addTotals(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
all() {
|
||||
return this.repo.find();
|
||||
}
|
||||
|
||||
addTotals(group: GroupEntity) {
|
||||
for (let m of group.members) {
|
||||
const expenses = group.expenses.filter(ex => ex.splits.some( s => s.member.id == m.id));
|
||||
const total = expenses.reduce((a, b) => a + b.amount * ((b.splits || []).find(s => s.member.id == m.id)?.weight || 0), 0);
|
||||
m.balance = m.balance - total;
|
||||
}
|
||||
}
|
||||
|
||||
createGroup(
|
||||
dto: CreateGroupDto,
|
||||
manager?: EntityManager,
|
||||
|
||||
@ -1,9 +1,20 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import * as session from 'express-session';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// somewhere in your initialization file
|
||||
app.use(
|
||||
session({
|
||||
secret: 'sdfjsodjf',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
}),
|
||||
);
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true, // entfernt unbekannte Felder
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user