inmemory to postgres

This commit is contained in:
2025-12-29 18:34:12 +01:00
parent 9e17d0752a
commit f5639a367f
176 changed files with 10175 additions and 468 deletions

View File

@@ -0,0 +1,34 @@
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
@Entity({ name: 'payments_member_payments' })
export class PaymentsMemberPaymentOrmEntity {
@PrimaryColumn({ type: 'text' })
id!: string;
@Index()
@Column({ type: 'text' })
feeId!: string;
@Index()
@Column({ type: 'text' })
driverId!: string;
@Column({ type: 'double precision' })
amount!: number;
@Column({ type: 'double precision' })
platformFee!: number;
@Column({ type: 'double precision' })
netAmount!: number;
@Index()
@Column({ type: 'text' })
status!: string;
@Column({ type: 'timestamptz' })
dueDate!: Date;
@Column({ type: 'timestamptz', nullable: true })
paidAt!: Date | null;
}

View File

@@ -0,0 +1,29 @@
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
@Entity({ name: 'payments_membership_fees' })
export class PaymentsMembershipFeeOrmEntity {
@PrimaryColumn({ type: 'text' })
id!: string;
@Index({ unique: true })
@Column({ type: 'text' })
leagueId!: string;
@Column({ type: 'text', nullable: true })
seasonId!: string | null;
@Column({ type: 'text' })
type!: string;
@Column({ type: 'double precision' })
amount!: number;
@Column({ type: 'boolean' })
enabled!: boolean;
@Column({ type: 'timestamptz' })
createdAt!: Date;
@Column({ type: 'timestamptz' })
updatedAt!: Date;
}

View File

@@ -0,0 +1,44 @@
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
@Entity({ name: 'payments_payments' })
export class PaymentsPaymentOrmEntity {
@PrimaryColumn({ type: 'text' })
id!: string;
@Index()
@Column({ type: 'text' })
leagueId!: string;
@Index()
@Column({ type: 'text' })
payerId!: string;
@Index()
@Column({ type: 'text' })
type!: string;
@Column({ type: 'double precision' })
amount!: number;
@Column({ type: 'double precision' })
platformFee!: number;
@Column({ type: 'double precision' })
netAmount!: number;
@Column({ type: 'text' })
payerType!: string;
@Column({ type: 'text', nullable: true })
seasonId!: string | null;
@Index()
@Column({ type: 'text' })
status!: string;
@Column({ type: 'timestamptz' })
createdAt!: Date;
@Column({ type: 'timestamptz', nullable: true })
completedAt!: Date | null;
}

View File

@@ -0,0 +1,43 @@
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
@Index('IDX_payments_prizes_league_season_position_unique', ['leagueId', 'seasonId', 'position'], { unique: true })
@Entity({ name: 'payments_prizes' })
export class PaymentsPrizeOrmEntity {
@PrimaryColumn({ type: 'text' })
id!: string;
@Index()
@Column({ type: 'text' })
leagueId!: string;
@Index()
@Column({ type: 'text' })
seasonId!: string;
@Column({ type: 'int' })
position!: number;
@Column({ type: 'text' })
name!: string;
@Column({ type: 'double precision' })
amount!: number;
@Column({ type: 'text' })
type!: string;
@Column({ type: 'text', nullable: true })
description!: string | null;
@Column({ type: 'boolean' })
awarded!: boolean;
@Column({ type: 'text', nullable: true })
awardedTo!: string | null;
@Column({ type: 'timestamptz', nullable: true })
awardedAt!: Date | null;
@Column({ type: 'timestamptz' })
createdAt!: Date;
}

View File

@@ -0,0 +1,29 @@
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
@Entity({ name: 'payments_wallet_transactions' })
export class PaymentsTransactionOrmEntity {
@PrimaryColumn({ type: 'text' })
id!: string;
@Index()
@Column({ type: 'text' })
walletId!: string;
@Column({ type: 'text' })
type!: string;
@Column({ type: 'double precision' })
amount!: number;
@Column({ type: 'text' })
description!: string;
@Column({ type: 'text', nullable: true })
referenceId!: string | null;
@Column({ type: 'text', nullable: true })
referenceType!: string | null;
@Column({ type: 'timestamptz' })
createdAt!: Date;
}

View File

@@ -0,0 +1,29 @@
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
@Entity({ name: 'payments_wallets' })
export class PaymentsWalletOrmEntity {
@PrimaryColumn({ type: 'text' })
id!: string;
@Index({ unique: true })
@Column({ type: 'text' })
leagueId!: string;
@Column({ type: 'double precision', default: 0 })
balance!: number;
@Column({ type: 'double precision', default: 0 })
totalRevenue!: number;
@Column({ type: 'double precision', default: 0 })
totalPlatformFees!: number;
@Column({ type: 'double precision', default: 0 })
totalWithdrawn!: number;
@Column({ type: 'text' })
currency!: string;
@Column({ type: 'timestamptz' })
createdAt!: Date;
}

View File

@@ -0,0 +1,32 @@
export type TypeOrmPaymentsSchemaErrorReason =
| 'missing'
| 'not_string'
| 'empty_string'
| 'not_number'
| 'not_integer'
| 'not_boolean'
| 'not_date'
| 'invalid_date'
| 'invalid_enum_value'
| 'invalid_shape';
export class TypeOrmPaymentsSchemaError extends Error {
readonly entityName: string;
readonly fieldName: string;
readonly reason: TypeOrmPaymentsSchemaErrorReason | (string & {});
constructor(params: {
entityName: string;
fieldName: string;
reason: TypeOrmPaymentsSchemaError['reason'];
message?: string;
}) {
const message =
params.message ?? `Invalid persisted ${params.entityName}.${params.fieldName}: ${params.reason}`;
super(message);
this.name = 'TypeOrmPaymentsSchemaError';
this.entityName = params.entityName;
this.fieldName = params.fieldName;
this.reason = params.reason;
}
}

View File

@@ -0,0 +1,53 @@
import type { MemberPayment } from '@core/payments/domain/entities/MemberPayment';
import { MemberPayment as MemberPaymentFactory, MemberPaymentStatus } from '@core/payments/domain/entities/MemberPayment';
import { PaymentsMemberPaymentOrmEntity } from '../entities/PaymentsMemberPaymentOrmEntity';
import {
assertDate,
assertEnumValue,
assertNonEmptyString,
assertNumber,
assertOptionalDate,
} from '../schema/TypeOrmPaymentsSchemaGuards';
export class PaymentsMemberPaymentOrmMapper {
toOrmEntity(domain: MemberPayment): PaymentsMemberPaymentOrmEntity {
const entity = new PaymentsMemberPaymentOrmEntity();
entity.id = domain.id;
entity.feeId = domain.feeId;
entity.driverId = domain.driverId;
entity.amount = domain.amount;
entity.platformFee = domain.platformFee;
entity.netAmount = domain.netAmount;
entity.status = domain.status;
entity.dueDate = domain.dueDate;
entity.paidAt = domain.paidAt ?? null;
return entity;
}
toDomain(entity: PaymentsMemberPaymentOrmEntity): MemberPayment {
const entityName = 'PaymentsMemberPayment';
assertNonEmptyString(entityName, 'id', entity.id);
assertNonEmptyString(entityName, 'feeId', entity.feeId);
assertNonEmptyString(entityName, 'driverId', entity.driverId);
assertNumber(entityName, 'amount', entity.amount);
assertNumber(entityName, 'platformFee', entity.platformFee);
assertNumber(entityName, 'netAmount', entity.netAmount);
assertEnumValue(entityName, 'status', entity.status, Object.values(MemberPaymentStatus));
assertDate(entityName, 'dueDate', entity.dueDate);
assertOptionalDate(entityName, 'paidAt', entity.paidAt);
return MemberPaymentFactory.rehydrate({
id: entity.id,
feeId: entity.feeId,
driverId: entity.driverId,
amount: entity.amount,
platformFee: entity.platformFee,
netAmount: entity.netAmount,
status: entity.status,
dueDate: entity.dueDate,
...(entity.paidAt !== null && entity.paidAt !== undefined ? { paidAt: entity.paidAt } : {}),
});
}
}

View File

@@ -0,0 +1,51 @@
import type { MembershipFee } from '@core/payments/domain/entities/MembershipFee';
import { MembershipFee as MembershipFeeFactory, MembershipFeeType } from '@core/payments/domain/entities/MembershipFee';
import { PaymentsMembershipFeeOrmEntity } from '../entities/PaymentsMembershipFeeOrmEntity';
import {
assertBoolean,
assertDate,
assertEnumValue,
assertNonEmptyString,
assertNumber,
assertOptionalStringOrNull,
} from '../schema/TypeOrmPaymentsSchemaGuards';
export class PaymentsMembershipFeeOrmMapper {
toOrmEntity(domain: MembershipFee): PaymentsMembershipFeeOrmEntity {
const entity = new PaymentsMembershipFeeOrmEntity();
entity.id = domain.id;
entity.leagueId = domain.leagueId;
entity.seasonId = domain.seasonId ?? null;
entity.type = domain.type;
entity.amount = domain.amount;
entity.enabled = domain.enabled;
entity.createdAt = domain.createdAt;
entity.updatedAt = domain.updatedAt;
return entity;
}
toDomain(entity: PaymentsMembershipFeeOrmEntity): MembershipFee {
const entityName = 'PaymentsMembershipFee';
assertNonEmptyString(entityName, 'id', entity.id);
assertNonEmptyString(entityName, 'leagueId', entity.leagueId);
assertOptionalStringOrNull(entityName, 'seasonId', entity.seasonId);
assertEnumValue(entityName, 'type', entity.type, Object.values(MembershipFeeType));
assertNumber(entityName, 'amount', entity.amount);
assertBoolean(entityName, 'enabled', entity.enabled);
assertDate(entityName, 'createdAt', entity.createdAt);
assertDate(entityName, 'updatedAt', entity.updatedAt);
return MembershipFeeFactory.rehydrate({
id: entity.id,
leagueId: entity.leagueId,
type: entity.type,
amount: entity.amount,
enabled: entity.enabled,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
...(entity.seasonId !== null && entity.seasonId !== undefined ? { seasonId: entity.seasonId } : {}),
});
}
}

View File

@@ -0,0 +1,70 @@
import type { Payment } from '@core/payments/domain/entities/Payment';
import {
Payment as PaymentFactory,
PaymentStatus,
PaymentType,
PayerType,
} from '@core/payments/domain/entities/Payment';
import { PaymentsPaymentOrmEntity } from '../entities/PaymentsPaymentOrmEntity';
import {
assertDate,
assertEnumValue,
assertNonEmptyString,
assertNumber,
assertOptionalStringOrNull,
} from '../schema/TypeOrmPaymentsSchemaGuards';
export class PaymentsPaymentOrmMapper {
toOrmEntity(domain: Payment): PaymentsPaymentOrmEntity {
const entity = new PaymentsPaymentOrmEntity();
entity.id = domain.id;
entity.type = domain.type;
entity.amount = domain.amount;
entity.platformFee = domain.platformFee;
entity.netAmount = domain.netAmount;
entity.payerId = domain.payerId;
entity.payerType = domain.payerType;
entity.leagueId = domain.leagueId;
entity.seasonId = domain.seasonId ?? null;
entity.status = domain.status;
entity.createdAt = domain.createdAt;
entity.completedAt = domain.completedAt ?? null;
return entity;
}
toDomain(entity: PaymentsPaymentOrmEntity): Payment {
const entityName = 'PaymentsPayment';
assertNonEmptyString(entityName, 'id', entity.id);
assertEnumValue(entityName, 'type', entity.type, Object.values(PaymentType));
assertNumber(entityName, 'amount', entity.amount);
assertNumber(entityName, 'platformFee', entity.platformFee);
assertNumber(entityName, 'netAmount', entity.netAmount);
assertNonEmptyString(entityName, 'payerId', entity.payerId);
assertEnumValue(entityName, 'payerType', entity.payerType, Object.values(PayerType));
assertNonEmptyString(entityName, 'leagueId', entity.leagueId);
assertOptionalStringOrNull(entityName, 'seasonId', entity.seasonId);
assertEnumValue(entityName, 'status', entity.status, Object.values(PaymentStatus));
assertDate(entityName, 'createdAt', entity.createdAt);
if (entity.completedAt !== null && entity.completedAt !== undefined) {
assertDate(entityName, 'completedAt', entity.completedAt);
}
return PaymentFactory.rehydrate({
id: entity.id,
type: entity.type,
amount: entity.amount,
platformFee: entity.platformFee,
netAmount: entity.netAmount,
payerId: entity.payerId,
payerType: entity.payerType,
leagueId: entity.leagueId,
status: entity.status,
createdAt: entity.createdAt,
...(entity.seasonId !== null && entity.seasonId !== undefined ? { seasonId: entity.seasonId } : {}),
...(entity.completedAt !== null && entity.completedAt !== undefined ? { completedAt: entity.completedAt } : {}),
});
}
}

View File

@@ -0,0 +1,65 @@
import type { Prize } from '@core/payments/domain/entities/Prize';
import { Prize as PrizeFactory, PrizeType } from '@core/payments/domain/entities/Prize';
import { PaymentsPrizeOrmEntity } from '../entities/PaymentsPrizeOrmEntity';
import {
assertBoolean,
assertDate,
assertEnumValue,
assertInteger,
assertNonEmptyString,
assertNumber,
assertOptionalDate,
assertOptionalStringOrNull,
} from '../schema/TypeOrmPaymentsSchemaGuards';
export class PaymentsPrizeOrmMapper {
toOrmEntity(domain: Prize): PaymentsPrizeOrmEntity {
const entity = new PaymentsPrizeOrmEntity();
entity.id = domain.id;
entity.leagueId = domain.leagueId;
entity.seasonId = domain.seasonId;
entity.position = domain.position;
entity.name = domain.name;
entity.amount = domain.amount;
entity.type = domain.type;
entity.description = domain.description ?? null;
entity.awarded = domain.awarded;
entity.awardedTo = domain.awardedTo ?? null;
entity.awardedAt = domain.awardedAt ?? null;
entity.createdAt = domain.createdAt;
return entity;
}
toDomain(entity: PaymentsPrizeOrmEntity): Prize {
const entityName = 'PaymentsPrize';
assertNonEmptyString(entityName, 'id', entity.id);
assertNonEmptyString(entityName, 'leagueId', entity.leagueId);
assertNonEmptyString(entityName, 'seasonId', entity.seasonId);
assertInteger(entityName, 'position', entity.position);
assertNonEmptyString(entityName, 'name', entity.name);
assertNumber(entityName, 'amount', entity.amount);
assertEnumValue(entityName, 'type', entity.type, Object.values(PrizeType));
assertOptionalStringOrNull(entityName, 'description', entity.description);
assertBoolean(entityName, 'awarded', entity.awarded);
assertOptionalStringOrNull(entityName, 'awardedTo', entity.awardedTo);
assertOptionalDate(entityName, 'awardedAt', entity.awardedAt);
assertDate(entityName, 'createdAt', entity.createdAt);
return PrizeFactory.rehydrate({
id: entity.id,
leagueId: entity.leagueId,
seasonId: entity.seasonId,
position: entity.position,
name: entity.name,
amount: entity.amount,
type: entity.type,
awarded: entity.awarded,
createdAt: entity.createdAt,
...(entity.description !== null && entity.description !== undefined ? { description: entity.description } : {}),
...(entity.awardedTo !== null && entity.awardedTo !== undefined ? { awardedTo: entity.awardedTo } : {}),
...(entity.awardedAt !== null && entity.awardedAt !== undefined ? { awardedAt: entity.awardedAt } : {}),
});
}
}

View File

@@ -0,0 +1,68 @@
import { describe, expect, it } from 'vitest';
import { TransactionType } from '@core/payments/domain/entities/Wallet';
import { TypeOrmPaymentsSchemaError } from '../errors/TypeOrmPaymentsSchemaError';
import { PaymentsWalletOrmEntity } from '../entities/PaymentsWalletOrmEntity';
import { PaymentsWalletOrmMapper } from './PaymentsWalletOrmMapper';
describe('PaymentsWalletOrmMapper', () => {
it('maps Wallet domain <-> orm', () => {
const wallet = {
id: 'wallet-1',
leagueId: 'league-1',
balance: 10,
totalRevenue: 20,
totalPlatformFees: 1,
totalWithdrawn: 5,
currency: 'USD',
createdAt: new Date('2025-01-01T00:00:00.000Z'),
};
const mapper = new PaymentsWalletOrmMapper();
const orm = mapper.toOrmEntity(wallet);
const rehydrated = mapper.toDomain(orm);
expect(rehydrated.id).toBe('wallet-1');
expect(rehydrated.leagueId).toBe('league-1');
expect(rehydrated.balance).toBe(10);
expect(rehydrated.currency).toBe('USD');
expect(rehydrated.createdAt.toISOString()).toBe('2025-01-01T00:00:00.000Z');
});
it('throws schema error on invalid Wallet', () => {
const mapper = new PaymentsWalletOrmMapper();
const orm = new PaymentsWalletOrmEntity();
orm.id = '';
orm.leagueId = 'league-1';
orm.balance = 0;
orm.totalRevenue = 0;
orm.totalPlatformFees = 0;
orm.totalWithdrawn = 0;
orm.currency = 'USD';
orm.createdAt = new Date();
expect(() => mapper.toDomain(orm)).toThrow(TypeOrmPaymentsSchemaError);
});
it('maps Transaction domain <-> orm', () => {
const transaction = {
id: 'txn-1',
walletId: 'wallet-1',
type: TransactionType.DEPOSIT,
amount: 123,
description: 'Deposit',
createdAt: new Date('2025-01-01T00:00:00.000Z'),
};
const mapper = new PaymentsWalletOrmMapper();
const orm = mapper.toOrmTransaction(transaction);
const rehydrated = mapper.toDomainTransaction(orm);
expect(rehydrated.id).toBe('txn-1');
expect(rehydrated.walletId).toBe('wallet-1');
expect(rehydrated.type).toBe(TransactionType.DEPOSIT);
expect(rehydrated.amount).toBe(123);
});
});

View File

@@ -0,0 +1,93 @@
import type { Wallet, Transaction } from '@core/payments/domain/entities/Wallet';
import { ReferenceType, Transaction as TransactionFactory, TransactionType, Wallet as WalletFactory } from '@core/payments/domain/entities/Wallet';
import { PaymentsTransactionOrmEntity } from '../entities/PaymentsTransactionOrmEntity';
import { PaymentsWalletOrmEntity } from '../entities/PaymentsWalletOrmEntity';
import {
assertDate,
assertEnumValue,
assertNonEmptyString,
assertNumber,
assertOptionalStringOrNull,
} from '../schema/TypeOrmPaymentsSchemaGuards';
export class PaymentsWalletOrmMapper {
toOrmEntity(domain: Wallet): PaymentsWalletOrmEntity {
const entity = new PaymentsWalletOrmEntity();
entity.id = domain.id;
entity.leagueId = domain.leagueId;
entity.balance = domain.balance;
entity.totalRevenue = domain.totalRevenue;
entity.totalPlatformFees = domain.totalPlatformFees;
entity.totalWithdrawn = domain.totalWithdrawn;
entity.currency = domain.currency;
entity.createdAt = domain.createdAt;
return entity;
}
toDomain(entity: PaymentsWalletOrmEntity): Wallet {
const entityName = 'PaymentsWallet';
assertNonEmptyString(entityName, 'id', entity.id);
assertNonEmptyString(entityName, 'leagueId', entity.leagueId);
assertNumber(entityName, 'balance', entity.balance);
assertNumber(entityName, 'totalRevenue', entity.totalRevenue);
assertNumber(entityName, 'totalPlatformFees', entity.totalPlatformFees);
assertNumber(entityName, 'totalWithdrawn', entity.totalWithdrawn);
assertNonEmptyString(entityName, 'currency', entity.currency);
assertDate(entityName, 'createdAt', entity.createdAt);
return WalletFactory.rehydrate({
id: entity.id,
leagueId: entity.leagueId,
balance: entity.balance,
totalRevenue: entity.totalRevenue,
totalPlatformFees: entity.totalPlatformFees,
totalWithdrawn: entity.totalWithdrawn,
currency: entity.currency,
createdAt: entity.createdAt,
});
}
toOrmTransaction(domain: Transaction): PaymentsTransactionOrmEntity {
const entity = new PaymentsTransactionOrmEntity();
entity.id = domain.id;
entity.walletId = domain.walletId;
entity.type = domain.type;
entity.amount = domain.amount;
entity.description = domain.description;
entity.referenceId = domain.referenceId ?? null;
entity.referenceType = domain.referenceType ?? null;
entity.createdAt = domain.createdAt;
return entity;
}
toDomainTransaction(entity: PaymentsTransactionOrmEntity): Transaction {
const entityName = 'PaymentsTransaction';
assertNonEmptyString(entityName, 'id', entity.id);
assertNonEmptyString(entityName, 'walletId', entity.walletId);
assertNonEmptyString(entityName, 'type', entity.type);
assertEnumValue(entityName, 'type', entity.type, Object.values(TransactionType));
assertNumber(entityName, 'amount', entity.amount);
assertNonEmptyString(entityName, 'description', entity.description);
assertOptionalStringOrNull(entityName, 'referenceId', entity.referenceId);
assertOptionalStringOrNull(entityName, 'referenceType', entity.referenceType);
assertDate(entityName, 'createdAt', entity.createdAt);
if (entity.referenceType !== null && entity.referenceType !== undefined) {
assertEnumValue(entityName, 'referenceType', entity.referenceType, Object.values(ReferenceType));
}
return TransactionFactory.rehydrate({
id: entity.id,
walletId: entity.walletId,
type: entity.type,
amount: entity.amount,
description: entity.description,
createdAt: entity.createdAt,
...(entity.referenceId !== null && entity.referenceId !== undefined ? { referenceId: entity.referenceId } : {}),
...(entity.referenceType !== null && entity.referenceType !== undefined ? { referenceType: entity.referenceType as ReferenceType } : {}),
});
}
}

View File

@@ -0,0 +1,87 @@
import type { DataSource } from 'typeorm';
import type { IMemberPaymentRepository, IMembershipFeeRepository } from '@core/payments/domain/repositories/IMembershipFeeRepository';
import type { MemberPayment } from '@core/payments/domain/entities/MemberPayment';
import type { MembershipFee } from '@core/payments/domain/entities/MembershipFee';
import { PaymentsMemberPaymentOrmEntity } from '../entities/PaymentsMemberPaymentOrmEntity';
import { PaymentsMembershipFeeOrmEntity } from '../entities/PaymentsMembershipFeeOrmEntity';
import { PaymentsMemberPaymentOrmMapper } from '../mappers/PaymentsMemberPaymentOrmMapper';
import { PaymentsMembershipFeeOrmMapper } from '../mappers/PaymentsMembershipFeeOrmMapper';
export class TypeOrmMembershipFeeRepository implements IMembershipFeeRepository {
constructor(
private readonly dataSource: DataSource,
private readonly mapper: PaymentsMembershipFeeOrmMapper,
) {}
async findById(id: string): Promise<MembershipFee | null> {
const repo = this.dataSource.getRepository(PaymentsMembershipFeeOrmEntity);
const entity = await repo.findOne({ where: { id } });
return entity ? this.mapper.toDomain(entity) : null;
}
async findByLeagueId(leagueId: string): Promise<MembershipFee | null> {
const repo = this.dataSource.getRepository(PaymentsMembershipFeeOrmEntity);
const entity = await repo.findOne({ where: { leagueId } });
return entity ? this.mapper.toDomain(entity) : null;
}
async create(fee: MembershipFee): Promise<MembershipFee> {
const repo = this.dataSource.getRepository(PaymentsMembershipFeeOrmEntity);
await repo.save(this.mapper.toOrmEntity(fee));
return fee;
}
async update(fee: MembershipFee): Promise<MembershipFee> {
const repo = this.dataSource.getRepository(PaymentsMembershipFeeOrmEntity);
await repo.save(this.mapper.toOrmEntity(fee));
return fee;
}
}
export class TypeOrmMemberPaymentRepository implements IMemberPaymentRepository {
constructor(
private readonly dataSource: DataSource,
private readonly mapper: PaymentsMemberPaymentOrmMapper,
) {}
async findById(id: string): Promise<MemberPayment | null> {
const repo = this.dataSource.getRepository(PaymentsMemberPaymentOrmEntity);
const entity = await repo.findOne({ where: { id } });
return entity ? this.mapper.toDomain(entity) : null;
}
async findByFeeIdAndDriverId(feeId: string, driverId: string): Promise<MemberPayment | null> {
const repo = this.dataSource.getRepository(PaymentsMemberPaymentOrmEntity);
const entity = await repo.findOne({ where: { feeId, driverId } });
return entity ? this.mapper.toDomain(entity) : null;
}
async findByLeagueIdAndDriverId(
leagueId: string,
driverId: string,
membershipFeeRepo: IMembershipFeeRepository,
): Promise<MemberPayment[]> {
const fee = await membershipFeeRepo.findByLeagueId(leagueId);
if (!fee) {
return [];
}
const repo = this.dataSource.getRepository(PaymentsMemberPaymentOrmEntity);
const entities = await repo.find({ where: { feeId: fee.id, driverId } });
return entities.map((e) => this.mapper.toDomain(e));
}
async create(payment: MemberPayment): Promise<MemberPayment> {
const repo = this.dataSource.getRepository(PaymentsMemberPaymentOrmEntity);
await repo.save(this.mapper.toOrmEntity(payment));
return payment;
}
async update(payment: MemberPayment): Promise<MemberPayment> {
const repo = this.dataSource.getRepository(PaymentsMemberPaymentOrmEntity);
await repo.save(this.mapper.toOrmEntity(payment));
return payment;
}
}

View File

@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import type { DataSource } from 'typeorm';
import { TypeOrmPaymentRepository } from './TypeOrmPaymentRepository';
import { PaymentsPaymentOrmMapper } from '../mappers/PaymentsPaymentOrmMapper';
describe('TypeOrmPaymentRepository', () => {
it('constructor requires injected mapper (no internal mapper instantiation)', () => {
const dataSource = {} as unknown as DataSource;
const mapper = {} as unknown as PaymentsPaymentOrmMapper;
const repo = new TypeOrmPaymentRepository(dataSource, mapper);
expect(repo).toBeInstanceOf(TypeOrmPaymentRepository);
expect((repo as unknown as { mapper: unknown }).mapper).toBe(mapper);
});
it('works with mocked TypeORM DataSource (no DB required)', async () => {
const findOne = async () => null;
const dataSource = {
getRepository: () => ({ findOne }),
} as unknown as DataSource;
const mapper = {
toDomain: () => {
throw new Error('should-not-be-called');
},
} as unknown as PaymentsPaymentOrmMapper;
const repo = new TypeOrmPaymentRepository(dataSource, mapper);
await expect(repo.findById('payment-1')).resolves.toBeNull();
});
});

View File

@@ -0,0 +1,67 @@
import type { DataSource } from 'typeorm';
import type { IPaymentRepository } from '@core/payments/domain/repositories/IPaymentRepository';
import type { Payment, PaymentType } from '@core/payments/domain/entities/Payment';
import { PaymentsPaymentOrmEntity } from '../entities/PaymentsPaymentOrmEntity';
import { PaymentsPaymentOrmMapper } from '../mappers/PaymentsPaymentOrmMapper';
export class TypeOrmPaymentRepository implements IPaymentRepository {
constructor(
private readonly dataSource: DataSource,
private readonly mapper: PaymentsPaymentOrmMapper,
) {}
async findById(id: string): Promise<Payment | null> {
const repo = this.dataSource.getRepository(PaymentsPaymentOrmEntity);
const entity = await repo.findOne({ where: { id } });
return entity ? this.mapper.toDomain(entity) : null;
}
async findByLeagueId(leagueId: string): Promise<Payment[]> {
const repo = this.dataSource.getRepository(PaymentsPaymentOrmEntity);
const entities = await repo.find({ where: { leagueId } });
return entities.map((e) => this.mapper.toDomain(e));
}
async findByPayerId(payerId: string): Promise<Payment[]> {
const repo = this.dataSource.getRepository(PaymentsPaymentOrmEntity);
const entities = await repo.find({ where: { payerId } });
return entities.map((e) => this.mapper.toDomain(e));
}
async findByType(type: PaymentType): Promise<Payment[]> {
const repo = this.dataSource.getRepository(PaymentsPaymentOrmEntity);
const entities = await repo.find({ where: { type } });
return entities.map((e) => this.mapper.toDomain(e));
}
async findByFilters(filters: { leagueId?: string; payerId?: string; type?: PaymentType }): Promise<Payment[]> {
const repo = this.dataSource.getRepository(PaymentsPaymentOrmEntity);
const where: {
leagueId?: string;
payerId?: string;
type?: PaymentType;
} = {};
if (filters.leagueId !== undefined) where.leagueId = filters.leagueId;
if (filters.payerId !== undefined) where.payerId = filters.payerId;
if (filters.type !== undefined) where.type = filters.type;
const entities = await repo.find({ where });
return entities.map((e) => this.mapper.toDomain(e));
}
async create(payment: Payment): Promise<Payment> {
const repo = this.dataSource.getRepository(PaymentsPaymentOrmEntity);
await repo.save(this.mapper.toOrmEntity(payment));
return payment;
}
async update(payment: Payment): Promise<Payment> {
const repo = this.dataSource.getRepository(PaymentsPaymentOrmEntity);
await repo.save(this.mapper.toOrmEntity(payment));
return payment;
}
}

View File

@@ -0,0 +1,55 @@
import type { DataSource } from 'typeorm';
import type { IPrizeRepository } from '@core/payments/domain/repositories/IPrizeRepository';
import type { Prize } from '@core/payments/domain/entities/Prize';
import { PaymentsPrizeOrmEntity } from '../entities/PaymentsPrizeOrmEntity';
import { PaymentsPrizeOrmMapper } from '../mappers/PaymentsPrizeOrmMapper';
export class TypeOrmPrizeRepository implements IPrizeRepository {
constructor(
private readonly dataSource: DataSource,
private readonly mapper: PaymentsPrizeOrmMapper,
) {}
async findById(id: string): Promise<Prize | null> {
const repo = this.dataSource.getRepository(PaymentsPrizeOrmEntity);
const entity = await repo.findOne({ where: { id } });
return entity ? this.mapper.toDomain(entity) : null;
}
async findByLeagueId(leagueId: string): Promise<Prize[]> {
const repo = this.dataSource.getRepository(PaymentsPrizeOrmEntity);
const entities = await repo.find({ where: { leagueId } });
return entities.map((e) => this.mapper.toDomain(e));
}
async findByLeagueIdAndSeasonId(leagueId: string, seasonId: string): Promise<Prize[]> {
const repo = this.dataSource.getRepository(PaymentsPrizeOrmEntity);
const entities = await repo.find({ where: { leagueId, seasonId } });
return entities.map((e) => this.mapper.toDomain(e));
}
async findByPosition(leagueId: string, seasonId: string, position: number): Promise<Prize | null> {
const repo = this.dataSource.getRepository(PaymentsPrizeOrmEntity);
const entity = await repo.findOne({ where: { leagueId, seasonId, position } });
return entity ? this.mapper.toDomain(entity) : null;
}
async create(prize: Prize): Promise<Prize> {
const repo = this.dataSource.getRepository(PaymentsPrizeOrmEntity);
await repo.save(this.mapper.toOrmEntity(prize));
return prize;
}
async update(prize: Prize): Promise<Prize> {
const repo = this.dataSource.getRepository(PaymentsPrizeOrmEntity);
await repo.save(this.mapper.toOrmEntity(prize));
return prize;
}
async delete(id: string): Promise<void> {
const repo = this.dataSource.getRepository(PaymentsPrizeOrmEntity);
await repo.delete({ id });
}
}

View File

@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import type { DataSource } from 'typeorm';
import { TypeOrmWalletRepository } from './TypeOrmWalletRepository';
import { PaymentsWalletOrmMapper } from '../mappers/PaymentsWalletOrmMapper';
describe('TypeOrmWalletRepository', () => {
it('constructor requires injected mapper (no internal mapper instantiation)', () => {
const dataSource = {} as unknown as DataSource;
const mapper = {} as unknown as PaymentsWalletOrmMapper;
const repo = new TypeOrmWalletRepository(dataSource, mapper);
expect(repo).toBeInstanceOf(TypeOrmWalletRepository);
expect((repo as unknown as { mapper: unknown }).mapper).toBe(mapper);
});
it('works with mocked TypeORM DataSource (no DB required)', async () => {
const findOne = async () => null;
const dataSource = {
getRepository: () => ({ findOne }),
} as unknown as DataSource;
const mapper = {
toDomain: () => {
throw new Error('should-not-be-called');
},
} as unknown as PaymentsWalletOrmMapper;
const repo = new TypeOrmWalletRepository(dataSource, mapper);
await expect(repo.findById('wallet-1')).resolves.toBeNull();
});
});

View File

@@ -0,0 +1,64 @@
import type { DataSource } from 'typeorm';
import type { ITransactionRepository, IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
import type { Transaction, Wallet } from '@core/payments/domain/entities/Wallet';
import { PaymentsTransactionOrmEntity } from '../entities/PaymentsTransactionOrmEntity';
import { PaymentsWalletOrmEntity } from '../entities/PaymentsWalletOrmEntity';
import { PaymentsWalletOrmMapper } from '../mappers/PaymentsWalletOrmMapper';
export class TypeOrmWalletRepository implements IWalletRepository {
constructor(
private readonly dataSource: DataSource,
private readonly mapper: PaymentsWalletOrmMapper,
) {}
async findById(id: string): Promise<Wallet | null> {
const repo = this.dataSource.getRepository(PaymentsWalletOrmEntity);
const entity = await repo.findOne({ where: { id } });
return entity ? this.mapper.toDomain(entity) : null;
}
async findByLeagueId(leagueId: string): Promise<Wallet | null> {
const repo = this.dataSource.getRepository(PaymentsWalletOrmEntity);
const entity = await repo.findOne({ where: { leagueId } });
return entity ? this.mapper.toDomain(entity) : null;
}
async create(wallet: Wallet): Promise<Wallet> {
const repo = this.dataSource.getRepository(PaymentsWalletOrmEntity);
await repo.save(this.mapper.toOrmEntity(wallet));
return wallet;
}
async update(wallet: Wallet): Promise<Wallet> {
const repo = this.dataSource.getRepository(PaymentsWalletOrmEntity);
await repo.save(this.mapper.toOrmEntity(wallet));
return wallet;
}
}
export class TypeOrmTransactionRepository implements ITransactionRepository {
constructor(
private readonly dataSource: DataSource,
private readonly mapper: PaymentsWalletOrmMapper,
) {}
async findById(id: string): Promise<Transaction | null> {
const repo = this.dataSource.getRepository(PaymentsTransactionOrmEntity);
const entity = await repo.findOne({ where: { id } });
return entity ? this.mapper.toDomainTransaction(entity) : null;
}
async findByWalletId(walletId: string): Promise<Transaction[]> {
const repo = this.dataSource.getRepository(PaymentsTransactionOrmEntity);
const entities = await repo.find({ where: { walletId } });
return entities.map((e) => this.mapper.toDomainTransaction(e));
}
async create(transaction: Transaction): Promise<Transaction> {
const repo = this.dataSource.getRepository(PaymentsTransactionOrmEntity);
await repo.save(this.mapper.toOrmTransaction(transaction));
return transaction;
}
}

View File

@@ -0,0 +1,120 @@
import { TypeOrmPaymentsSchemaError } from '../errors/TypeOrmPaymentsSchemaError';
export function assertNonEmptyString(
entityName: string,
fieldName: string,
value: unknown,
): asserts value is string {
if (typeof value !== 'string') {
throw new TypeOrmPaymentsSchemaError({ entityName, fieldName, reason: 'not_string' });
}
if (value.trim().length === 0) {
throw new TypeOrmPaymentsSchemaError({ entityName, fieldName, reason: 'empty_string' });
}
}
export function assertNumber(entityName: string, fieldName: string, value: unknown): asserts value is number {
if (typeof value !== 'number' || Number.isNaN(value)) {
throw new TypeOrmPaymentsSchemaError({ entityName, fieldName, reason: 'not_number' });
}
}
export function assertInteger(entityName: string, fieldName: string, value: unknown): asserts value is number {
if (typeof value !== 'number' || !Number.isInteger(value)) {
throw new TypeOrmPaymentsSchemaError({ entityName, fieldName, reason: 'not_integer' });
}
}
export function assertBoolean(entityName: string, fieldName: string, value: unknown): asserts value is boolean {
if (typeof value !== 'boolean') {
throw new TypeOrmPaymentsSchemaError({ entityName, fieldName, reason: 'not_boolean' });
}
}
export function assertDate(entityName: string, fieldName: string, value: unknown): asserts value is Date {
if (!(value instanceof Date)) {
throw new TypeOrmPaymentsSchemaError({ entityName, fieldName, reason: 'not_date' });
}
if (Number.isNaN(value.getTime())) {
throw new TypeOrmPaymentsSchemaError({ entityName, fieldName, reason: 'invalid_date' });
}
}
export function assertEnumValue<TAllowed extends string>(
entityName: string,
fieldName: string,
value: unknown,
allowed: readonly TAllowed[],
): asserts value is TAllowed {
if (typeof value !== 'string') {
throw new TypeOrmPaymentsSchemaError({ entityName, fieldName, reason: 'not_string' });
}
if (!allowed.includes(value as TAllowed)) {
throw new TypeOrmPaymentsSchemaError({ entityName, fieldName, reason: 'invalid_enum_value' });
}
}
export function assertOptionalStringOrNull(
entityName: string,
fieldName: string,
value: unknown,
): asserts value is string | null | undefined {
if (value === null || value === undefined) {
return;
}
if (typeof value !== 'string') {
throw new TypeOrmPaymentsSchemaError({ entityName, fieldName, reason: 'not_string' });
}
}
export function assertOptionalDate(
entityName: string,
fieldName: string,
value: unknown,
): asserts value is Date | undefined | null {
if (value === undefined || value === null) {
return;
}
assertDate(entityName, fieldName, value);
}
export function assertOptionalNumber(
entityName: string,
fieldName: string,
value: unknown,
): asserts value is number | undefined | null {
if (value === undefined || value === null) {
return;
}
assertNumber(entityName, fieldName, value);
}
export function assertOptionalBoolean(
entityName: string,
fieldName: string,
value: unknown,
): asserts value is boolean | undefined | null {
if (value === undefined || value === null) {
return;
}
assertBoolean(entityName, fieldName, value);
}
export const TypeOrmPaymentsSchemaGuards = {
assertNonEmptyString,
assertNumber,
assertInteger,
assertBoolean,
assertDate,
assertEnumValue,
assertOptionalStringOrNull,
assertOptionalDate,
assertOptionalNumber,
assertOptionalBoolean,
} as const;