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,73 @@
import {
MigrationInterface,
QueryRunner,
TableColumn,
TableColumnOptions,
} from 'typeorm';

export class EnhanceSavingsProductWithTvlAndRiskLevel1775200000000
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
// Add tvlAmount column if it doesn't exist
const table = await queryRunner.getTable('savings_products');

if (table && !table.findColumnByName('tvlAmount')) {
await queryRunner.addColumn(
'savings_products',
new TableColumn({
name: 'tvlAmount',
type: 'decimal',
precision: 14,
scale: 2,
default: 0,
isNullable: false,
}),
);
}

// Update riskLevel column from varchar to enum
if (table && table.findColumnByName('riskLevel')) {
const riskLevelColumn = table.findColumnByName('riskLevel');

if (riskLevelColumn && riskLevelColumn.type !== 'enum') {
// Drop the old constraint and column with old data
await queryRunner.changeColumn(
'savings_products',
'riskLevel',
new TableColumn({
name: 'riskLevel',
type: 'enum',
enum: ['LOW', 'MEDIUM', 'HIGH'],
default: "'LOW'",
isNullable: false,
}),
);
}
}
}

public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable('savings_products');

// Remove tvlAmount column
if (table && table.findColumnByName('tvlAmount')) {
await queryRunner.dropColumn('savings_products', 'tvlAmount');
}

// Revert riskLevel column back to varchar
if (table && table.findColumnByName('riskLevel')) {
await queryRunner.changeColumn(
'savings_products',
'riskLevel',
new TableColumn({
name: 'riskLevel',
type: 'varchar',
length: '20',
default: "'Low'",
isNullable: false,
}),
);
}
}
}
29 changes: 28 additions & 1 deletion backend/src/modules/savings/dto/create-product.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
MaxLength,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { SavingsProductType } from '../entities/savings-product.entity';
import { SavingsProductType, RiskLevel } from '../entities/savings-product.entity';

export class CreateProductDto {
@ApiProperty({ example: 'Fixed 12-Month Plan', description: 'Product name' })
Expand Down Expand Up @@ -56,6 +56,33 @@ export class CreateProductDto {
@Max(360)
tenureMonths?: number;

@ApiPropertyOptional({
example: 'contract1234567890abcdefghijklmnopqrstuvwxyz',
description: 'Soroban contract ID for testnet/mainnet',
})
@IsOptional()
@IsString()
@MaxLength(56)
contractId?: string;

@ApiPropertyOptional({
description: 'Total Value Locked amount',
default: 0,
})
@IsOptional()
@IsNumber()
@Min(0)
tvlAmount?: number;

@ApiPropertyOptional({
enum: RiskLevel,
default: RiskLevel.LOW,
description: 'Risk level classification',
})
@IsOptional()
@IsEnum(RiskLevel)
riskLevel?: RiskLevel;

@ApiPropertyOptional({ default: true })
@IsOptional()
isActive?: boolean;
Expand Down
6 changes: 3 additions & 3 deletions backend/src/modules/savings/dto/product-details.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { SavingsProductType } from '../entities/savings-product.entity';
import { SavingsProductType, RiskLevel } from '../entities/savings-product.entity';

/**
* Detailed product response combining static DB attributes with live Soroban contract data
Expand Down Expand Up @@ -46,8 +46,8 @@ export class ProductDetailsDto {
@ApiProperty({ description: 'Product creation timestamp' })
createdAt: Date;

@ApiProperty({ description: 'Risk level classification' })
riskLevel: string;
@ApiProperty({ description: 'Risk level classification', enum: RiskLevel })
riskLevel: RiskLevel;

@ApiProperty({ description: 'Product last update timestamp' })
updatedAt: Date;
Expand Down
5 changes: 3 additions & 2 deletions backend/src/modules/savings/dto/savings-product.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { SavingsProductType } from '../entities/savings-product.entity';
import { SavingsProductType, RiskLevel } from '../entities/savings-product.entity';

export class SavingsProductDto {
@ApiProperty({ description: 'Product UUID' })
Expand Down Expand Up @@ -34,8 +34,9 @@ export class SavingsProductDto {

@ApiProperty({
description: 'Risk level classification (e.g. Low, Medium, High)',
enum: RiskLevel,
})
riskLevel: string;
riskLevel: RiskLevel;

@ApiProperty({ description: 'Total Value Locked (aggregated local balance)' })
tvlAmount: number;
Expand Down
13 changes: 11 additions & 2 deletions backend/src/modules/savings/entities/savings-product.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export enum SavingsProductType {
FLEXIBLE = 'FLEXIBLE',
}

export enum RiskLevel {
LOW = 'LOW',
MEDIUM = 'MEDIUM',
HIGH = 'HIGH',
}

@Entity('savings_products')
export class SavingsProduct {
@PrimaryGeneratedColumn('uuid')
Expand Down Expand Up @@ -42,11 +48,14 @@ export class SavingsProduct {
@Column({ type: 'varchar', length: 56, nullable: true })
contractId: string | null;

@Column('decimal', { precision: 14, scale: 2, default: 0 })
tvlAmount: number;

@Column({ default: true })
isActive: boolean;

@Column({ type: 'varchar', length: 20, default: 'Low' })
riskLevel: string;
@Column({ type: 'enum', enum: RiskLevel, default: RiskLevel.LOW })
riskLevel: RiskLevel;

@CreateDateColumn()
createdAt: Date;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SavingsService } from './savings.service';
import {
SavingsProduct,
SavingsProductType,
RiskLevel,
} from './entities/savings-product.entity';
import {
UserSubscription,
Expand All @@ -33,15 +34,15 @@ describe('SavingsController (Enhanced)', () => {
{ amount: 100, status: SubscriptionStatus.ACTIVE },
{ amount: 50, status: SubscriptionStatus.ACTIVE },
],
riskLevel: 'Low',
riskLevel: RiskLevel.LOW,
},
{
id: 'p2',
name: 'Beta Pool',
interestRate: 15,
createdAt: new Date('2026-01-02'),
subscriptions: [{ amount: 30, status: SubscriptionStatus.ACTIVE }],
riskLevel: 'Medium',
riskLevel: RiskLevel.MEDIUM,
},
];

Expand Down Expand Up @@ -93,6 +94,6 @@ describe('SavingsController (Enhanced)', () => {
it('should include riskLevel in the response', async () => {
const result = await controller.getProducts();
expect(result[0].riskLevel).toBeDefined();
expect(['Low', 'Medium', 'High']).toContain(result[0].riskLevel);
expect(Object.values(RiskLevel)).toContain(result[0].riskLevel);
});
});
4 changes: 2 additions & 2 deletions backend/src/modules/savings/savings.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { SavingsService } from './savings.service';
import { SavingsProduct } from './entities/savings-product.entity';
import { SavingsProduct, RiskLevel } from './entities/savings-product.entity';
import { UserSubscription } from './entities/user-subscription.entity';
import { SavingsGoal } from './entities/savings-goal.entity';
import { SubscribeDto } from './dto/subscribe.dto';
Expand Down Expand Up @@ -109,7 +109,7 @@ export class SavingsController {
contractId: product.contractId,
totalAssets,
totalAssetsXlm,
riskLevel: (product as any).riskLevel || 'Low',
riskLevel: product.riskLevel || RiskLevel.LOW,
createdAt: product.createdAt,
updatedAt: product.updatedAt,
};
Expand Down
4 changes: 2 additions & 2 deletions backend/src/modules/savings/savings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Cache } from 'cache-manager';
import { Repository } from 'typeorm';
import { SavingsProduct } from './entities/savings-product.entity';
import { SavingsProduct, RiskLevel } from './entities/savings-product.entity';
import {
UserSubscription,
SubscriptionStatus,
Expand Down Expand Up @@ -122,7 +122,7 @@ export class SavingsService {
tenureMonths: product.tenureMonths,
contractId: product.contractId,
isActive: product.isActive,
riskLevel: (product as any).riskLevel || 'Low',
riskLevel: product.riskLevel || RiskLevel.LOW,
tvlAmount,
createdAt: product.createdAt,
updatedAt: product.updatedAt,
Expand Down
Loading