From 6b5abfcb5fbfc36c17e1e45a7ba8c34d8f522ebf Mon Sep 17 00:00:00 2001 From: The Pantseller Date: Sun, 29 Mar 2026 05:46:36 +0000 Subject: [PATCH] feat(savings): add estimatedYieldPerSecond to subscription response (#409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #409, #410, #411, #412 - #409: Compute estimatedYieldPerSecond = (liveBalance * annualRate) / (365*24*3600) and expose it on every UserSubscriptionWithLiveBalance item returned by GET /savings/my-subscriptions, enabling frontend ticker animations without polling the API. - #410: Redis cache layer via @CacheKey('pools_all') + @CacheTTL(60000) on GET /savings/products and manual invalidation via cacheManager.del('pools_all') on any pool mutation — already in place, verified by existing tests. - #411: GET /analytics/portfolio?timeframe=[1D|1W|1M|YTD] with enum validation and backward-reconstruction timeline — already in place, verified by existing tests. - #412: GET /analytics/allocation grouping holdings by assetId, dividing by total for decimal-precise percentages sorted highest-first — already in place. --- backend/src/modules/savings/savings.service.spec.ts | 5 +++++ backend/src/modules/savings/savings.service.ts | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/backend/src/modules/savings/savings.service.spec.ts b/backend/src/modules/savings/savings.service.spec.ts index 1d5db9ce6..398928ffc 100644 --- a/backend/src/modules/savings/savings.service.spec.ts +++ b/backend/src/modules/savings/savings.service.spec.ts @@ -228,6 +228,7 @@ describe('SavingsService', () => { userId: 'user-1', productId: 'product-1', amount: 12.5, + product: { interestRate: 10 }, createdAt: new Date('2026-01-01'), }, ]); @@ -246,6 +247,8 @@ describe('SavingsService', () => { balanceSource: 'rpc', vaultContractId: 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M', + // 2.5 * 0.10 / (365 * 24 * 3600) ≈ 0.0000000079 + estimatedYieldPerSecond: expect.any(Number), }), ]); expect(blockchainSavingsService.getUserVaultBalance).toHaveBeenCalledWith( @@ -261,6 +264,7 @@ describe('SavingsService', () => { userId: 'user-1', productId: 'product-1', amount: 8.75, + product: { interestRate: 5 }, createdAt: new Date('2026-01-01'), }, ]); @@ -275,6 +279,7 @@ describe('SavingsService', () => { liveBalance: 8.75, balanceSource: 'cache', vaultContractId: null, + estimatedYieldPerSecond: expect.any(Number), }), ]); expect(blockchainSavingsService.getUserVaultBalance).not.toHaveBeenCalled(); diff --git a/backend/src/modules/savings/savings.service.ts b/backend/src/modules/savings/savings.service.ts index cc4f479e8..bea60d232 100644 --- a/backend/src/modules/savings/savings.service.ts +++ b/backend/src/modules/savings/savings.service.ts @@ -32,6 +32,7 @@ export interface UserSubscriptionWithLiveBalance extends UserSubscription { liveBalanceStroops: number; balanceSource: 'rpc' | 'cache'; vaultContractId: string | null; + estimatedYieldPerSecond: number; } const STROOPS_PER_XLM = 10_000_000; @@ -451,6 +452,10 @@ export class SavingsService { balanceSource: 'rpc' | 'cache', vaultContractId: string | null, ): UserSubscriptionWithLiveBalance { + const annualRate = Number(subscription.product?.interestRate ?? 0) / 100; + const estimatedYieldPerSecond = parseFloat( + ((liveBalance * annualRate) / (365 * 24 * 3600)).toFixed(10), + ); return { ...subscription, indexedAmount: Number(subscription.amount), @@ -458,6 +463,7 @@ export class SavingsService { liveBalanceStroops, balanceSource, vaultContractId, + estimatedYieldPerSecond, }; }