Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm';

export class AddSavingsProductVersioning1791100000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumns('savings_products', [
new TableColumn({
name: 'version',
type: 'int',
default: 1,
}),
new TableColumn({
name: 'versionGroupId',
type: 'uuid',
isNullable: true,
}),
new TableColumn({
name: 'previousVersionId',
type: 'uuid',
isNullable: true,
}),
]);

await queryRunner.createTable(
new Table({
name: 'savings_product_version_audits',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
isGenerated: true,
generationStrategy: 'uuid',
},
{ name: 'productId', type: 'uuid' },
{ name: 'versionGroupId', type: 'uuid' },
{ name: 'sourceProductId', type: 'uuid', isNullable: true },
{ name: 'targetProductId', type: 'uuid', isNullable: true },
{ name: 'actorId', type: 'uuid', isNullable: true },
{ name: 'action', type: 'varchar' },
{ name: 'metadata', type: 'jsonb', isNullable: true },
{ name: 'createdAt', type: 'timestamp', default: 'now()' },
],
}),
);

await queryRunner.query(`
UPDATE savings_products
SET "versionGroupId" = id
WHERE "versionGroupId" IS NULL
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('savings_product_version_audits');
await queryRunner.dropColumn('savings_products', 'previousVersionId');
await queryRunner.dropColumn('savings_products', 'versionGroupId');
await queryRunner.dropColumn('savings_products', 'version');
}
}
25 changes: 25 additions & 0 deletions backend/src/modules/admin/admin-savings.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../common/guards/roles.guard';
import { Roles } from '../../common/decorators/roles.decorator';
import { Role } from '../../common/enums/role.enum';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { ProductCapacitySnapshot } from '../savings/savings.service';
import { PageOptionsDto } from '../../common/dto/page-options.dto';
import { CreateProductDto } from '../savings/dto/create-product.dto';
Expand Down Expand Up @@ -82,6 +83,30 @@ export class AdminSavingsController {
return this.adminSavingsService.getSubscribers(id, opts);
}

@Post('products/:id/migrations')
@ApiOperation({
summary: 'Migrate active subscriptions to another product version (admin)',
})
@ApiResponse({
status: 200,
description: 'Subscriptions migrated to the target product version',
})
async migrateProductSubscriptions(
@Param('id') id: string,
@Body() body: { targetProductId: string; subscriptionIds?: string[] },
@CurrentUser() user: { id: string; email: string },
): Promise<{ migratedCount: number; targetProductId: string }> {
const result = await this.savingsService.migrateSubscriptionsToVersion(
id,
body.targetProductId,
user.id,
body.subscriptionIds,
);

return {
migratedCount: result.migratedCount,
targetProductId: result.targetProduct.id,
};
@Get('products/:id/capacity-metrics')
@ApiOperation({ summary: 'Get live capacity utilization metrics (admin)' })
@ApiResponse({
Expand Down
10 changes: 10 additions & 0 deletions backend/src/modules/savings/dto/create-product.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ export class CreateProductDto {
@IsEnum(RiskLevel)
riskLevel?: RiskLevel;

@ApiPropertyOptional({
example: 1,
description: 'Initial product version',
default: 1,
})
@IsOptional()
@IsNumber()
@Min(1)
version?: number;

@ApiPropertyOptional({ default: true })
@IsOptional()
isActive?: boolean;
Expand Down
3 changes: 3 additions & 0 deletions backend/src/modules/savings/dto/product-details.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export class ProductDetailsDto {
@ApiProperty({ description: 'Whether product is active' })
isActive: boolean;

@ApiProperty({ description: 'Current product version' })
version: number;

@ApiPropertyOptional({ description: 'Soroban vault contract ID' })
contractId: string | null;

Expand Down
3 changes: 3 additions & 0 deletions backend/src/modules/savings/dto/savings-product.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export class SavingsProductDto {
@ApiProperty({ description: 'Whether product is active' })
isActive: boolean;

@ApiProperty({ description: 'Current product version' })
version: number;

@ApiProperty({
description: 'Risk level classification (e.g. Low, Medium, High)',
enum: RiskLevel,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
Index,
} from 'typeorm';

export type SavingsProductVersionAuditAction =
| 'CREATED'
| 'UPDATED'
| 'VERSION_CREATED'
| 'SUBSCRIPTIONS_MIGRATED';

@Entity('savings_product_version_audits')
@Index(['productId', 'createdAt'])
@Index(['versionGroupId', 'createdAt'])
export class SavingsProductVersionAudit {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column('uuid')
productId: string;

@Column('uuid')
versionGroupId: string;

@Column({ type: 'uuid', nullable: true })
sourceProductId: string | null;

@Column({ type: 'uuid', nullable: true })
targetProductId: string | null;

@Column({ type: 'uuid', nullable: true })
actorId: string | null;

@Column({ type: 'varchar' })
action: SavingsProductVersionAuditAction;

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

@CreateDateColumn()
createdAt: Date;
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ export class SavingsProduct {
@Column('int', { nullable: true })
capacity: number | null;

@Column({ type: 'int', default: 1 })
version: number;

@Column({ type: 'uuid', nullable: true })
versionGroupId: string | null;

@Column({ type: 'uuid', nullable: true })
previousVersionId: string | null;
@Column('decimal', { precision: 14, scale: 2, nullable: true })
maxCapacity: number | null;

Expand Down
1 change: 1 addition & 0 deletions backend/src/modules/savings/savings.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export class SavingsController {
minAmount: product.minAmount,
maxAmount: product.maxAmount,
tenureMonths: product.tenureMonths,
version: product.version ?? 1,
isActive: product.isActive,
contractId: product.contractId,
totalAssets,
Expand Down
Loading
Loading