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
26 changes: 12 additions & 14 deletions apps/api/src/billing/http-schemas/usage.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,27 @@ export const GetUsageHistoryQuerySchema = z
description: "Start date (YYYY-MM-DD). Defaults to 30 days before endDate",
example: "2024-01-01"
}),
endDate: z
.string()
.date()
.default(() => new Date().toISOString().split("T")[0])
.openapi({
description: "End date (YYYY-MM-DD). Defaults to today by UTC 23:59:59",
example: "2024-01-31"
})
endDate: z.string().date().optional().openapi({
description: "End date (YYYY-MM-DD). Defaults to today by UTC 23:59:59",
example: "2024-01-31"
})
})
.transform(data => {
const endDate = data.endDate ?? new Date().toISOString().split("T")[0];

if (data.startDate) {
return data;
return { ...data, startDate: data.startDate, endDate };
}

const endDate = new Date(data.endDate);
endDate.setDate(endDate.getDate() - 30);
const startDate = new Date(endDate);
startDate.setDate(startDate.getDate() - 30);

return { ...data, startDate: endDate.toISOString().split("T")[0] };
return { ...data, startDate: startDate.toISOString().split("T")[0], endDate };
})
.refine(
data => {
const end = new Date(data.endDate!);
const start = new Date(data.startDate!);
const end = new Date(data.endDate);
const start = new Date(data.startDate);

const daysDiff = Math.ceil((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000));

Expand Down
8 changes: 4 additions & 4 deletions apps/api/src/billing/http-schemas/wallet.schema.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { z } from "@hono/zod-openapi";

const WalletOutputSchema = z.object({
id: z.number().nullable().openapi({}),
userId: z.string().nullable().openapi({}),
id: z.number().openapi({}),
userId: z.string().openapi({}),
creditAmount: z.number().openapi({}),
address: z.string().nullable().openapi({}),
address: z.string().openapi({}),
denom: z.string().openapi({}),
isTrialing: z.boolean(),
topUpMinAmountUsd: z.number().openapi({ description: "Minimum USD amount accepted by the next paid top-up for this wallet." }),
createdAt: z.coerce.date().nullable().openapi({})
createdAt: z.date().openapi({})
});

const WalletWithOptional3DSSchema = WalletOutputSchema.extend({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ export type UserWalletOutput = Omit<DbUserWalletOutput, "feeAllowance" | "deploy
feeAllowance: number;
};

export type WalletInitialized = Omit<UserWalletOutput, "address"> & { address: string };

export interface UserWalletPublicOutput {
id: UserWalletOutput["id"];
userId: UserWalletOutput["userId"];
address: UserWalletOutput["address"];
address: WalletInitialized["address"];
creditAmount: UserWalletOutput["creditAmount"];
isTrialing: boolean;
createdAt: UserWalletOutput["createdAt"];
Expand Down Expand Up @@ -164,7 +166,7 @@ export class UserWalletRepository extends BaseRepository<ApiPgTables["UserWallet
return dbInput;
}

toPublic(output: UserWalletOutput): UserWalletPublicOutput {
toPublic(output: WalletInitialized): UserWalletPublicOutput {
return {
id: output.id,
userId: output.userId,
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/billing/routes/usage/usage.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ export const usageRouter = new OpenApiHonoHandler();

usageRouter.openapi(getUsageHistoryRoute, async function routeGetBillingUsage(c) {
const { address, startDate, endDate } = c.req.valid("query");
const usageHistory = await container.resolve(UsageController).getHistory(address, startDate!, endDate!);
const usageHistory = await container.resolve(UsageController).getHistory(address, startDate, endDate);
return c.json(usageHistory);
});

usageRouter.openapi(getUsageHistoryStatsRoute, async function routeGetBillingOverview(c) {
const { address, startDate, endDate } = c.req.valid("query");
const usageHistoryStats = await container.resolve(UsageController).getHistoryStats(address, startDate!, endDate!);
const usageHistoryStats = await container.resolve(UsageController).getHistoryStats(address, startDate, endDate);
return c.json(usageHistoryStats);
});
10 changes: 5 additions & 5 deletions apps/api/src/billing/services/refill/refill.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { RefillService } from "@src/billing/services/refill/refill.service";
import type { WalletInitializerService } from "@src/billing/services/wallet-initializer/wallet-initializer.service";
import type { AnalyticsService } from "@src/core/services/analytics/analytics.service";

import { createUserWallet } from "@test/seeders/user-wallet.seeder";
import { createInitializedUserWallet, createUserWallet } from "@test/seeders/user-wallet.seeder";

describe(RefillService.name, () => {
describe("topUpWallet", () => {
Expand All @@ -19,7 +19,7 @@ describe(RefillService.name, () => {
it("should top up existing activated wallet", async () => {
const { service, userWalletRepository, managedUserWalletService, managedSignerService, balancesService, walletInitializerService, analyticsService } =
setup();
const existingWallet = createUserWallet({ userId });
const existingWallet = createInitializedUserWallet({ userId });
walletInitializerService.ensureWallet.mockResolvedValue(existingWallet);
userWalletRepository.claimActivation.mockResolvedValue(undefined);
managedUserWalletService.authorizeSpending.mockResolvedValue();
Expand All @@ -40,7 +40,7 @@ describe(RefillService.name, () => {

it("attaches payment context to the balance_top_up analytics event", async () => {
const { service, userWalletRepository, managedUserWalletService, balancesService, analyticsService, walletInitializerService } = setup();
const existingWallet = createUserWallet({ userId });
const existingWallet = createInitializedUserWallet({ userId });
walletInitializerService.ensureWallet.mockResolvedValue(existingWallet);
userWalletRepository.claimActivation.mockResolvedValue(undefined);
managedUserWalletService.authorizeSpending.mockResolvedValue();
Expand Down Expand Up @@ -72,7 +72,7 @@ describe(RefillService.name, () => {

it("does not end trial when endTrial option is false", async () => {
const { service, userWalletRepository, managedUserWalletService, balancesService, walletInitializerService } = setup();
const existingWallet = createUserWallet({ userId });
const existingWallet = createInitializedUserWallet({ userId });
walletInitializerService.ensureWallet.mockResolvedValue(existingWallet);
userWalletRepository.claimActivation.mockResolvedValue(undefined);
managedUserWalletService.authorizeSpending.mockResolvedValue();
Expand All @@ -87,7 +87,7 @@ describe(RefillService.name, () => {
it("activates a non-activated wallet on first funding", async () => {
const { service, userWalletRepository, walletInitializerService, balancesService, managedUserWalletService, managedSignerService, analyticsService } =
setup();
const wallet = createUserWallet({ userId, activatedAt: null });
const wallet = createInitializedUserWallet({ userId, activatedAt: null });
const activatedWallet = { ...wallet, activatedAt: new Date() };
walletInitializerService.ensureWallet.mockResolvedValue(wallet);
userWalletRepository.claimActivation.mockResolvedValue(activatedWallet);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { singleton } from "tsyringe";

import { AuthService } from "@src/auth/services/auth.service";
import { TrialStarted } from "@src/billing/events/trial-started";
import { UserWalletOutput, UserWalletPublicOutput, UserWalletRepository } from "@src/billing/repositories";
import { type UserWalletOutput, type UserWalletPublicOutput, UserWalletRepository, type WalletInitialized } from "@src/billing/repositories";
import { ManagedSignerService } from "@src/billing/services/managed-signer/managed-signer.service";
import { StripeService } from "@src/billing/services/stripe/stripe.service";
import { DomainEventsService } from "@src/core/services/domain-events/domain-events.service";
Expand Down Expand Up @@ -69,27 +69,28 @@ export class WalletInitializerService {

await this.domainEvents.publish(new TrialStarted({ userId }));

return this.userWalletRepository.toPublic(activatedWallet);
return this.userWalletRepository.toPublic({ ...activatedWallet, address: userWallet.address });
}

/**
* Idempotently guarantees the user has a wallet row with a derived address.
* Address derivation is pure (no chain transaction), so this is safe to run on every registration.
*/
async ensureWallet(userId: string): Promise<UserWalletOutput> {
async ensureWallet(userId: string): Promise<WalletInitialized> {
return this.#ensureWalletVia(this.userWalletRepository, userId);
}

/**
* Concurrent calls may both derive the address, but derivation is deterministic per wallet id,
* so the two updates write the same value and the operation stays idempotent.
*/
async #ensureWalletVia(repository: UserWalletRepository, userId: string): Promise<UserWalletOutput> {
async #ensureWalletVia(repository: UserWalletRepository, userId: string): Promise<WalletInitialized> {
const { wallet } = await repository.getOrCreate({ userId });

if (wallet.address) return wallet;
if (wallet.address) return { ...wallet, address: wallet.address };

const { address } = await this.walletManager.createWallet({ addressIndex: wallet.id });
return await this.userWalletRepository.updateById(wallet.id, { address }, { returning: true });
const updatedWallet = await repository.updateById(wallet.id, { address }, { returning: true });
return { ...updatedWallet, address };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import assert from "http-assert";
import { Lifecycle, scoped } from "tsyringe";

import { AuthService } from "@src/auth/services/auth.service";
import { UserWalletOutput, UserWalletPublicOutput, UserWalletRepository } from "@src/billing/repositories";
import { UserWalletOutput, UserWalletPublicOutput, UserWalletRepository, WalletInitialized } from "@src/billing/repositories";

export interface GetWalletOptions {
userId: string;
}

export type WalletInitialized = Omit<UserWalletOutput, "address"> & { address: string };

@scoped(Lifecycle.ResolutionScoped)
export class WalletReaderService {
constructor(
Expand All @@ -21,7 +19,9 @@ export class WalletReaderService {
async getWallets(query: GetWalletOptions): Promise<UserWalletPublicOutput[]> {
const wallets = await this.userWalletRepository.accessibleBy(this.authService.ability, "read").find(query);

return wallets.filter(wallet => wallet.activatedAt).map(wallet => this.userWalletRepository.toPublic(wallet));
return wallets
.filter((wallet): wallet is WalletInitialized => wallet.activatedAt !== null && !!wallet.address)
.map(wallet => this.userWalletRepository.toPublic(wallet));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

async getWalletByUserId(userId: string): Promise<WalletInitialized>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { AxiosError } from "axios";
import { describe, expect, it, vi } from "vitest";
import { mock } from "vitest-mock-extended";

import type { WalletInitialized, WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
import type { WalletInitialized } from "@src/billing/repositories";
import type { WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
import type { LoggerService } from "@src/core/providers/logging.provider";
import type { FallbackDeploymentReaderService } from "@src/deployment/services/fallback-deployment-reader/fallback-deployment-reader.service";
import type { FallbackLeaseReaderService } from "@src/deployment/services/fallback-lease-reader/fallback-lease-reader.service";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { InternalServerError } from "http-errors";
import { Op } from "sequelize";
import { singleton } from "tsyringe";

import { WalletInitialized, WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
import type { WalletInitialized } from "@src/billing/repositories";
import { WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
import { Memoize } from "@src/caching/helpers";
import { LoggerService } from "@src/core";
import { GetDeploymentResponse, ListDeploymentsItem } from "@src/deployment/http-schemas/deployment.schema";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { MsgCloseDeployment, MsgCreateDeployment, MsgUpdateDeployment } from "@a
import { afterEach, describe, expect, it, vi } from "vitest";
import { mock, type MockProxy } from "vitest-mock-extended";

import type { WalletInitialized } from "@src/billing/repositories";
import type { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service";
import type { ManagedSignerService } from "@src/billing/services/managed-signer/managed-signer.service";
import type { RpcMessageService } from "@src/billing/services/rpc-message-service/rpc-message.service";
import type { WalletInitialized, WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
import type { WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
import type { GetDeploymentResponse } from "@src/deployment/http-schemas/deployment.schema";
import type { SdlService } from "@src/deployment/services/sdl/sdl.service";
import type { ProviderService } from "@src/provider/services/provider/provider.service";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { manifestToSortedJSON } from "@akashnetwork/chain-sdk";
import assert from "http-assert";
import { singleton } from "tsyringe";

import { UserWalletOutput } from "@src/billing/repositories";
import { UserWalletOutput, WalletInitialized } from "@src/billing/repositories";
import { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service";
import { ManagedSignerService } from "@src/billing/services/managed-signer/managed-signer.service";
import { RpcMessageService } from "@src/billing/services/rpc-message-service/rpc-message.service";
import { WalletInitialized, WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
import { WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
import {
CreateDeploymentRequest,
CreateDeploymentResponse,
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/deployment/services/lease/lease.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import type { LeaseHttpService } from "@akashnetwork/http-sdk";
import { describe, expect, it } from "vitest";
import { mock } from "vitest-mock-extended";

import type { WalletInitialized } from "@src/billing/repositories";
import type { ManagedSignerService, RpcMessageService } from "@src/billing/services";
import type { WalletInitialized, WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
import type { WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
import type { GetDeploymentResponse } from "@src/deployment/http-schemas/deployment.schema";
import type { DeploymentReaderService } from "@src/deployment/services/deployment-reader/deployment-reader.service";
import type { ProviderService } from "@src/provider/services/provider/provider.service";
Expand Down
Loading