diff --git a/costly-api/package-lock.json b/costly-api/package-lock.json index 0337cff..fac45c7 100644 --- a/costly-api/package-lock.json +++ b/costly-api/package-lock.json @@ -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", diff --git a/costly-api/package.json b/costly-api/package.json index 9175f18..63d40c1 100644 --- a/costly-api/package.json +++ b/costly-api/package.json @@ -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", diff --git a/costly-api/src/app.module.ts b/costly-api/src/app.module.ts index 12ffec2..6fd086f 100644 --- a/costly-api/src/app.module.ts +++ b/costly-api/src/app.module.ts @@ -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], diff --git a/costly-api/src/database/database.module.ts b/costly-api/src/database/database.module.ts index 7f44f03..d8308b4 100644 --- a/costly-api/src/database/database.module.ts +++ b/costly-api/src/database/database.module.ts @@ -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] diff --git a/costly-api/src/expenses/dto/create-expense.dto.ts b/costly-api/src/expenses/dto/create-expense.dto.ts index a8c084d..3084330 100644 --- a/costly-api/src/expenses/dto/create-expense.dto.ts +++ b/costly-api/src/expenses/dto/create-expense.dto.ts @@ -1,5 +1,5 @@ export class CreateExpenseDto { name: string; amount: number; - groupId: number; + groupId: string; } \ No newline at end of file diff --git a/costly-api/src/expenses/expenses.module.ts b/costly-api/src/expenses/expenses.module.ts index 42cc44b..d4042d7 100644 --- a/costly-api/src/expenses/expenses.module.ts +++ b/costly-api/src/expenses/expenses.module.ts @@ -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] diff --git a/costly-api/src/expenses/persistence/expense-split.entity.ts b/costly-api/src/expenses/persistence/expense-split.entity.ts new file mode 100644 index 0000000..679c81a --- /dev/null +++ b/costly-api/src/expenses/persistence/expense-split.entity.ts @@ -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; +} \ No newline at end of file diff --git a/costly-api/src/expenses/persistence/expense.entity.ts b/costly-api/src/expenses/persistence/expense.entity.ts index 051fe53..eec3bc6 100644 --- a/costly-api/src/expenses/persistence/expense.entity.ts +++ b/costly-api/src/expenses/persistence/expense.entity.ts @@ -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; diff --git a/costly-api/src/group-events/api/group-events.controller.ts b/costly-api/src/group-events/api/group-events.controller.ts index 6d01945..7b3dcf2 100644 --- a/costly-api/src/group-events/api/group-events.controller.ts +++ b/costly-api/src/group-events/api/group-events.controller.ts @@ -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) { + return this.groupsEventsService.getLastEvents({}); } @Get(':groupdId') getGroupEvents(@Param('groupId') groupId: string) { - return this.groupsEventsService.getLastTenEvents(groupId); + return this.groupsEventsService.getLastEvents({groupId}); } } diff --git a/costly-api/src/group-events/application/group-events.service.ts b/costly-api/src/group-events/application/group-events.service.ts index 0dc1a45..7f62bf9 100644 --- a/costly-api/src/group-events/application/group-events.service.ts +++ b/costly-api/src/group-events/application/group-events.service.ts @@ -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; + } } diff --git a/costly-api/src/group-events/dto/group-event.dto.ts b/costly-api/src/group-events/dto/group-event.dto.ts index 4b70e48..56e746b 100644 --- a/costly-api/src/group-events/dto/group-event.dto.ts +++ b/costly-api/src/group-events/dto/group-event.dto.ts @@ -12,4 +12,56 @@ export class IncomingGroupEventDto { @IsOptional() @IsISO8601() clientCreatedAt?: string; -} \ No newline at end of file +} + +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 = { + id: string; + type: T; + payload: GroupEventPayloadByType[T]; + actorId?: string; + clientCreatedAt?: string; +}; diff --git a/costly-api/src/group-events/group-events.module.ts b/costly-api/src/group-events/group-events.module.ts index fd05966..a1ef280 100644 --- a/costly-api/src/group-events/group-events.module.ts +++ b/costly-api/src/group-events/group-events.module.ts @@ -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 {} diff --git a/costly-api/src/groups/groups.module.ts b/costly-api/src/groups/groups.module.ts index 75c56e5..0b78285 100644 --- a/costly-api/src/groups/groups.module.ts +++ b/costly-api/src/groups/groups.module.ts @@ -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] }) diff --git a/costly-api/src/groups/persistence/group-member.entity.ts b/costly-api/src/groups/persistence/group-member.entity.ts index d4f31b6..70f1f3a 100644 --- a/costly-api/src/groups/persistence/group-member.entity.ts +++ b/costly-api/src/groups/persistence/group-member.entity.ts @@ -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; diff --git a/costly-api/src/groups/persistence/group.entity.ts b/costly-api/src/groups/persistence/group.entity.ts index 205172e..dca3b9c 100644 --- a/costly-api/src/groups/persistence/group.entity.ts +++ b/costly-api/src/groups/persistence/group.entity.ts @@ -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[]; diff --git a/costly-api/src/groups/persistence/group.repository.ts b/costly-api/src/groups/persistence/group.repository.ts index 6941955..4bd9323 100644 --- a/costly-api/src/groups/persistence/group.repository.ts +++ b/costly-api/src/groups/persistence/group.repository.ts @@ -12,14 +12,24 @@ export class GroupsRepository { private readonly repo: Repository, ) {} - 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, diff --git a/costly-api/src/main.ts b/costly-api/src/main.ts index 49efa03..8878c0a 100644 --- a/costly-api/src/main.ts +++ b/costly-api/src/main.ts @@ -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