Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/database/migrations.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('Database migrations integration', () => {
});

it('applies every migration, matches entity metadata, and enforces foreign keys', () => {
expect(result.executedMigrationNames).toHaveLength(8);
expect(result.executedMigrationNames).toHaveLength(9);
expect(result.schemaInSync).toBe(true);
expect(result.enumValues).toEqual(Object.values(AccountStatus));
expect(result.foreignKeyColumns).toContainEqual(['accountId']);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddDeletedAtToAccountsTable1718100008000
implements MigrationInterface
{
name = 'AddDeletedAtToAccountsTable1718100008000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "accounts"
ADD COLUMN "deletedAt" timestamp NULL
`);

// Index to keep soft-delete filtering fast on high-traffic queries,
// mirroring the style of 1718100006000-AddHighTrafficIndexes.
await queryRunner.query(`
CREATE INDEX "IDX_accounts_deletedAt" ON "accounts" ("deletedAt")
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP INDEX "IDX_accounts_deletedAt"
`);

await queryRunner.query(`
ALTER TABLE "accounts"
DROP COLUMN "deletedAt"
`);
}
}
12 changes: 8 additions & 4 deletions src/modules/accounts/accounts.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ describe('AccountsService', () => {
function makeQueryBuilder(accounts: Account[], total: number) {
const qb = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([accounts, total]),
Expand All @@ -247,13 +248,16 @@ describe('AccountsService', () => {
mockRepo.createQueryBuilder.mockReturnValue(
makeQueryBuilder(accounts, 1),
);

const result = await service.findAll({ limit: 50, offset: 0 });

expect(result.total).toBe(1);
expect(result.accounts).toHaveLength(1);
});

it('excludes soft-deleted accounts by default', async () => {
const qb = makeQueryBuilder([], 0);
mockRepo.createQueryBuilder.mockReturnValue(qb);
await service.findAll({ limit: 50, offset: 0 });
expect(qb.where).toHaveBeenCalledWith('account.deletedAt IS NULL');
});
it('applies status filter when provided', async () => {
const qb = makeQueryBuilder([], 0);
mockRepo.createQueryBuilder.mockReturnValue(qb);
Expand All @@ -264,7 +268,7 @@ describe('AccountsService', () => {
offset: 0,
});

expect(qb.where).toHaveBeenCalledWith(
expect(qb.andWhere).toHaveBeenCalledWith(
'account.status = :status',
expect.objectContaining({ status: AccountStatus.PENDING_PAYMENT }),
);
Expand Down
6 changes: 4 additions & 2 deletions src/modules/accounts/accounts.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,12 @@ export class AccountsService {
limit: number;
offset: number;
}): Promise<{ accounts: AccountResponseDto[]; total: number }> {
const query = this.accountsRepository.createQueryBuilder('account');
const query = this.accountsRepository
.createQueryBuilder('account')
.where('account.deletedAt IS NULL');

if (status) {
query.where('account.status = :status', { status });
query.andWhere('account.status = :status', { status });
}

query.skip(offset).take(Math.min(limit, 100));
Expand Down
7 changes: 6 additions & 1 deletion src/modules/accounts/entities/account.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
} from 'typeorm';
import { AccountStatus } from '../enums/account-status.enum.js';
Expand All @@ -22,6 +23,7 @@ import { AccountStatus } from '../enums/account-status.enum.js';
@Index('IDX_accounts_status_expiresAt', ['status', 'expiresAt'])
@Index('IDX_accounts_status_createdAt', ['status', 'createdAt'])
@Index('IDX_accounts_createdAt', ['createdAt'])
@Index('IDX_accounts_deletedAt', ['deletedAt'])
@Entity('accounts')
export class Account {
@PrimaryGeneratedColumn('uuid')
Expand Down Expand Up @@ -79,6 +81,9 @@ export class Account {
@Column({ type: 'timestamp', nullable: true })
expiredAt: Date | null; // Actual time expiry was processed - set by the expiry handler, null until then

@Column({ type: 'jsonb', nullable: true })
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any>;

@DeleteDateColumn({ type: 'timestamp', nullable: true })
deletedAt: Date | null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ describe('ClaimRedemptionProvider', () => {
const qb = {
setLock: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
getOne: jest.fn().mockResolvedValue(lockedAccount),
};
return {
Expand Down
1 change: 1 addition & 0 deletions src/modules/claims/providers/claim-redemption.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export class ClaimRedemptionProvider {
.createQueryBuilder(Account, 'account')
.setLock('pessimistic_write')
.where('account.claimTokenHash = :tokenHash', { tokenHash })
.andWhere('account.deletedAt IS NULL')
.getOne();

if (!locked) {
Expand Down
1 change: 1 addition & 0 deletions test/migrations.integration.runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ async function main(): Promise<void> {
log: () => Promise<SqlInMemoryLog>;
}
).log();
console.error('DEBUG upQueries:', JSON.stringify(schemaLog.upQueries.map((q) => q.query), null, 2));

const enumRows: Array<{ enumlabel: string }> = await dataSource.query(`
SELECT e.enumlabel
Expand Down
Loading