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",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
|
"express-session": "^1.18.2",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
@ -31,6 +32,7 @@
|
|||||||
"@swc/cli": "^0.6.0",
|
"@swc/cli": "^0.6.0",
|
||||||
"@swc/core": "^1.10.7",
|
"@swc/core": "^1.10.7",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/express-session": "^1.18.2",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
@ -3362,6 +3364,16 @@
|
|||||||
"@types/send": "*"
|
"@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": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.9",
|
"version": "4.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||||
@ -6368,6 +6380,46 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"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": {
|
"node_modules/express/node_modules/content-disposition": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||||
@ -9072,6 +9124,15 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@ -9626,6 +9687,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
@ -11456,6 +11526,18 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/uint8array-extras": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
|
"express-session": "^1.18.2",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
@ -42,6 +43,7 @@
|
|||||||
"@swc/cli": "^0.6.0",
|
"@swc/cli": "^0.6.0",
|
||||||
"@swc/core": "^1.10.7",
|
"@swc/core": "^1.10.7",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/express-session": "^1.18.2",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
|
|||||||
@ -6,16 +6,31 @@ import { DatabaseModule } from './database/database.module';
|
|||||||
import { GroupsModule } from './groups/groups.module';
|
import { GroupsModule } from './groups/groups.module';
|
||||||
import { ExpensesModule } from './expenses/expenses.module';
|
import { ExpensesModule } from './expenses/expenses.module';
|
||||||
import { GroupEventsModule } from './group-events/group-events.module';
|
import { GroupEventsModule } from './group-events/group-events.module';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule.forRoot({
|
imports: [ConfigModule.forRoot({
|
||||||
envFilePath: '.env',
|
envFilePath: '.env',
|
||||||
isGlobal: true
|
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,
|
GroupsModule,
|
||||||
ExpensesModule,
|
ExpensesModule,
|
||||||
GroupEventsModule
|
GroupEventsModule,
|
||||||
|
DatabaseModule
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|||||||
@ -6,26 +6,14 @@ import { GroupEventEntity } from 'src/groups/persistence/group-event.entity';
|
|||||||
import { ExpenseEntity } from 'src/expenses/persistence/expense.entity';
|
import { ExpenseEntity } from 'src/expenses/persistence/expense.entity';
|
||||||
import { ExpenseEventEntity } from 'src/expenses/persistence/expense-event.entity';
|
import { ExpenseEventEntity } from 'src/expenses/persistence/expense-event.entity';
|
||||||
import { GroupMemberEntity } from 'src/groups/persistence/group-member.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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forFeature(ENTITY)
|
||||||
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',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
],
|
],
|
||||||
providers: [ConfigService,],
|
providers: [ConfigService,],
|
||||||
exports: [TypeOrmModule]
|
exports: [TypeOrmModule]
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export class CreateExpenseDto {
|
export class CreateExpenseDto {
|
||||||
name: string;
|
name: string;
|
||||||
amount: number;
|
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 { ExpensesService } from './application/expenses/expenses.service';
|
||||||
import { ExpensesController } from './api/expenses/expenses.controller';
|
import { ExpensesController } from './api/expenses/expenses.controller';
|
||||||
import { ExpensesRepository } from './persistence/expense.repository';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
DatabaseModule,
|
DatabaseModule
|
||||||
TypeOrmModule.forFeature([ExpenseEntity, ExpenseEventEntity])
|
|
||||||
],
|
],
|
||||||
providers: [ExpensesService, ExpensesRepository],
|
providers: [ExpensesService, ExpensesRepository],
|
||||||
controllers: [ExpensesController]
|
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 { 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')
|
@Entity('expenses')
|
||||||
export class ExpenseEntity {
|
export class ExpenseEntity {
|
||||||
@ -10,9 +11,12 @@ export class ExpenseEntity {
|
|||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@OneToMany(type => GroupEntity, group => group.expenses)
|
@ManyToOne(type => GroupEntity, group => group.expenses)
|
||||||
group: GroupEntity;
|
group: GroupEntity;
|
||||||
|
|
||||||
|
@OneToMany(type => ExpenseSplitEntity, split => split.expense, { eager: true, cascade: true})
|
||||||
|
splits: ExpenseSplitEntity[];
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
amount: number;
|
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 { GroupEventsService } from '../application/group-events.service';
|
||||||
import { IncomingGroupEventDto } from '../dto/group-event.dto';
|
import { IncomingGroupEventDto } from '../dto/group-event.dto';
|
||||||
|
|
||||||
@ -17,13 +17,13 @@ export class GroupEventsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
getEvents() {
|
getEvents(@Session() session: Record<string, any>) {
|
||||||
return this.groupsEventsService.getLastTenEvents();
|
return this.groupsEventsService.getLastEvents({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':groupdId')
|
@Get(':groupdId')
|
||||||
getGroupEvents(@Param('groupId') groupId: string) {
|
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 { GroupsRepository } from 'src/groups/persistence/group.repository';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { IncomingEventDto } from 'src/model/dto/incoming-event.dto';
|
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 { GroupMemberEntity } from 'src/groups/persistence/group-member.entity';
|
||||||
|
import { ExpenseSplitEntity } from 'src/expenses/persistence/expense-split.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GroupEventsService {
|
export class GroupEventsService {
|
||||||
@ -57,13 +58,15 @@ export class GroupEventsService {
|
|||||||
// optional: nur diejenigen anwenden, die "gerade neu" sind
|
// optional: nur diejenigen anwenden, die "gerade neu" sind
|
||||||
// (Wenn du ganz sauber sein willst: execute() liefert affected, aber nicht IDs.
|
// (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!)
|
// 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') {
|
if (ev.type === 'GROUP_RENAMED') {
|
||||||
await this.ingestGroupRenameEvent(groupId, ev)
|
await this.ingestGroupRenameEvent(groupId, ev as any)
|
||||||
} else if (ev.type == 'MEMBER_CREATED') {
|
} else if (ev.type == 'MEMBER_CREATED') {
|
||||||
await this.ingestGroupAddMemberEvent(groupId, ev);
|
await this.ingestGroupAddMemberEvent(groupId, ev as any);
|
||||||
} else if (ev.type == 'MEMBER_RENAMED') {
|
} 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.to) { throw new BadRequestException('to is required'); }
|
||||||
if (!ev.payload.from) { throw new BadRequestException('from is required'); }
|
if (!ev.payload.from) { throw new BadRequestException('from is required'); }
|
||||||
try {
|
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;
|
member.name = ev.payload.to;
|
||||||
await this.groupMembers.save(member);
|
await this.groupMembers.save(member);
|
||||||
return 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 group = await this.groupsRepo.findOneByOrFail({ id: groupId });
|
||||||
const member = await this.groupMembers.save(this.groupMembers.create({
|
const member = await this.groupMembers.save(this.groupMembers.create({
|
||||||
name: ev.payload.name,
|
name: ev.payload.name,
|
||||||
@ -92,20 +95,43 @@ export class GroupEventsService {
|
|||||||
}))
|
}))
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
async ingestGroupRenameEvent(groupId: string, ev: GroupEventEntity) {
|
async ingestGroupRenameEvent(groupId: string, ev: TypedIncomingGroupEvent<'GROUP_RENAMED'>) {
|
||||||
const group = await this.groupsRepo.findOneByOrFail({ id: groupId });
|
const group = await this.groupsRepo.findOneByOrFail({ id: groupId });
|
||||||
group.name = ev.payload.to;
|
group.name = ev.payload.to;
|
||||||
const saved = await this.groupsRepo.save(group);
|
const saved = await this.groupsRepo.save(group);
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
getLastTenEvents(groupId?: string) {
|
getLastEvents({groupId, amount}: {groupId?: string, amount?: 10}) {
|
||||||
const rep = this.dataSource.getRepository(GroupEventEntity);
|
const rep = this.dataSource.getRepository(GroupEventEntity);
|
||||||
return rep.find({
|
return rep.find({
|
||||||
where: { groupId },
|
where: { groupId },
|
||||||
order: { createdAt: 'DESC' },
|
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()
|
@IsOptional() @IsISO8601()
|
||||||
clientCreatedAt?: string;
|
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({
|
@Module({
|
||||||
controllers: [GroupEventsController],
|
controllers: [GroupEventsController],
|
||||||
providers: [GroupEventsService],
|
providers: [GroupEventsService],
|
||||||
imports: [DatabaseModule, TypeOrmModule.forFeature([GroupEventsController, GroupEntity, GroupMemberEntity])]
|
imports: [DatabaseModule]
|
||||||
})
|
})
|
||||||
export class GroupEventsModule {}
|
export class GroupEventsModule {}
|
||||||
|
|||||||
@ -5,9 +5,10 @@ import { GroupsController } from './api/groups.controller';
|
|||||||
import { GroupsService } from './application/groups.service';
|
import { GroupsService } from './application/groups.service';
|
||||||
import { GroupsRepository } from './persistence/group.repository';
|
import { GroupsRepository } from './persistence/group.repository';
|
||||||
import { GroupEventEntity } from './persistence/group-event.entity';
|
import { GroupEventEntity } from './persistence/group-event.entity';
|
||||||
|
import { DatabaseModule } from 'src/database/database.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([GroupEntity, GroupEventEntity])],
|
imports: [DatabaseModule],
|
||||||
controllers: [GroupsController],
|
controllers: [GroupsController],
|
||||||
providers: [GroupsService, GroupsRepository]
|
providers: [GroupsService, GroupsRepository]
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Column, CreateDateColumn, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
|
import { Column, CreateDateColumn, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
|
||||||
import { GroupEntity } from "./group.entity";
|
import { GroupEntity } from "./group.entity";
|
||||||
|
import { ExpenseSplitEntity } from "src/expenses/persistence/expense-split.entity";
|
||||||
|
|
||||||
@Entity('group_members')
|
@Entity('group_members')
|
||||||
export class GroupMemberEntity {
|
export class GroupMemberEntity {
|
||||||
@ -10,9 +11,14 @@ export class GroupMemberEntity {
|
|||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
balance: number = 0;
|
||||||
|
|
||||||
@ManyToOne(type => GroupEntity, group => group.members)
|
@ManyToOne(type => GroupEntity, group => group.members)
|
||||||
group: GroupEntity;
|
group: GroupEntity;
|
||||||
|
|
||||||
|
@OneToMany(type => ExpenseSplitEntity, split => split.member)
|
||||||
|
splits: ExpenseSplitEntity[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,8 @@ export class GroupEntity {
|
|||||||
@UpdateDateColumn({ name: 'updated_at' })
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
expenses: ExpenseEntity[] = [];
|
@OneToMany(type => ExpenseEntity, expense => expense.group, { eager: true })
|
||||||
|
expenses: ExpenseEntity[];
|
||||||
|
|
||||||
@OneToMany(type => GroupMemberEntity, member => member.group, { eager: true })
|
@OneToMany(type => GroupMemberEntity, member => member.group, { eager: true })
|
||||||
members: GroupMemberEntity[];
|
members: GroupMemberEntity[];
|
||||||
|
|||||||
@ -12,14 +12,24 @@ export class GroupsRepository {
|
|||||||
private readonly repo: Repository<GroupEntity>,
|
private readonly repo: Repository<GroupEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
findById(id: string) {
|
async findById(id: string) {
|
||||||
return this.repo.findOne({ where: { id } });
|
const group = await this.repo.findOneOrFail({ where: { id } });
|
||||||
|
this.addTotals(group);
|
||||||
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
all() {
|
all() {
|
||||||
return this.repo.find();
|
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(
|
createGroup(
|
||||||
dto: CreateGroupDto,
|
dto: CreateGroupDto,
|
||||||
manager?: EntityManager,
|
manager?: EntityManager,
|
||||||
|
|||||||
@ -1,9 +1,20 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import * as session from 'express-session';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
// somewhere in your initialization file
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
secret: 'sdfjsodjf',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
whitelist: true, // entfernt unbekannte Felder
|
whitelist: true, // entfernt unbekannte Felder
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user