From 40ee84a0b718aa1eabc1978beec5955b758934d2 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 3 Mar 2026 23:00:27 -0800 Subject: [PATCH] fix(billing-service): resolve all TypeScript compilation errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive fix of 124 TS errors across the billing-service: Entity fixes: - invoice.entity.ts: add InvoiceStatus/InvoiceCurrency const objects, rename fields to match DB schema (subtotalCents, taxCents, totalCents, amountDueCents), add OneToMany items relation - invoice-item.entity.ts: add InvoiceItemType const object, add column name mappings and currency field - payment.entity.ts: add PaymentStatus const, rename amount→amountCents with column name mapping, add paidAt field - subscription.entity.ts: add SubscriptionStatus const object - usage-aggregate.entity.ts: rename periodYear/Month→year/month to match DB columns, add periodStart/periodEnd fields - payment-method.entity.ts: add displayName, expiresAt, updatedAt fields Port/Provider fixes: - payment-provider.port.ts: make PaymentProviderType a const object (not just a type), add PaymentSessionRequest alias, rename WebhookEvent with correct field shape (type vs eventType), make providerPaymentId optional - All 4 providers: replace PaymentSessionRequest→CreatePaymentParams, fix amountCents→amount, remove sessionId from PaymentSession return, add confirmPayment() stub, fix Stripe API version to '2023-10-16' Use case fixes: - aggregate-usage.use-case.ts: replace 'redis' with 'ioredis' (workspace standard); rewrite using ioredis xreadgroup API - change/check/generate use cases: fix Plan field names (monthlyPriceCentsUsd, includedTokens, overageRateCentsPerMTokenUsd) - generate-monthly-invoice: fix SubscriptionStatus/InvoiceCurrency as values (now const objects) - handle-payment-webhook: fix WebhookResult import, result.type usage, payment.paidAt Controller/Repository fixes: - plan.controller.ts, plan.repository.ts: fix Plan field names - webhook.controller.ts: remove express import, use any for req type - invoice-generator.service.ts: fix overageAmountCents→overageCentsUsd, monthlyPriceCny→monthlyPriceFenCny, includedTokensPerMonth→includedTokens Dependencies: - billing-service/package.json: replace redis with ioredis dependency - pnpm-lock.yaml: regenerated after ioredis addition Co-Authored-By: Claude Sonnet 4.6 --- .../services/billing-service/package.json | 1 + .../use-cases/aggregate-usage.use-case.ts | 41 +++++++------- .../use-cases/change-plan.use-case.ts | 4 +- .../use-cases/check-token-quota.use-case.ts | 4 +- .../create-payment-session.use-case.ts | 6 +-- .../generate-monthly-invoice.use-case.ts | 10 ++-- .../handle-payment-webhook.use-case.ts | 2 +- .../domain/entities/invoice-item.entity.ts | 19 +++++-- .../src/domain/entities/invoice.entity.ts | 53 +++++++++++++------ .../domain/entities/payment-method.entity.ts | 21 +++++--- .../src/domain/entities/payment.entity.ts | 18 +++++-- .../domain/entities/subscription.entity.ts | 9 +++- .../domain/entities/usage-aggregate.entity.ts | 22 +++++--- .../src/domain/ports/payment-provider.port.ts | 28 +++++++--- .../services/invoice-generator.service.ts | 28 +++++----- .../alipay/alipay.provider.ts | 33 +++++++----- .../crypto/crypto.provider.ts | 23 ++++---- .../stripe/stripe.provider.ts | 31 +++++++---- .../wechat/wechat-pay.provider.ts | 21 ++++---- .../repositories/plan.repository.ts | 2 +- .../usage-aggregate.repository.ts | 6 +-- .../rest/controllers/plan.controller.ts | 8 +-- .../rest/controllers/webhook.controller.ts | 13 +++-- pnpm-lock.yaml | 3 ++ 24 files changed, 253 insertions(+), 153 deletions(-) diff --git a/packages/services/billing-service/package.json b/packages/services/billing-service/package.json index 3b3974b..419ff32 100644 --- a/packages/services/billing-service/package.json +++ b/packages/services/billing-service/package.json @@ -22,6 +22,7 @@ "stripe": "^14.0.0", "alipay-sdk": "^4.0.0", "coinbase-commerce-node": "^1.0.4", + "ioredis": "^5.3.0", "@it0/common": "workspace:*", "@it0/database": "workspace:*", "@it0/events": "workspace:*" diff --git a/packages/services/billing-service/src/application/use-cases/aggregate-usage.use-case.ts b/packages/services/billing-service/src/application/use-cases/aggregate-usage.use-case.ts index fdfa166..a40293e 100644 --- a/packages/services/billing-service/src/application/use-cases/aggregate-usage.use-case.ts +++ b/packages/services/billing-service/src/application/use-cases/aggregate-usage.use-case.ts @@ -1,6 +1,6 @@ import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { createClient, RedisClientType } from 'redis'; +import Redis from 'ioredis'; import { UsageAggregateRepository } from '../../infrastructure/repositories/usage-aggregate.repository'; interface UsageRecordedEvent { @@ -21,7 +21,7 @@ const CONSUMER_NAME = 'billing-consumer-1'; @Injectable() export class AggregateUsageUseCase implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(AggregateUsageUseCase.name); - private client: RedisClientType; + private client: Redis; private running = false; constructor( @@ -31,12 +31,11 @@ export class AggregateUsageUseCase implements OnModuleInit, OnModuleDestroy { async onModuleInit() { const redisUrl = this.configService.get('REDIS_URL', 'redis://localhost:6379'); - this.client = createClient({ url: redisUrl }) as RedisClientType; - await this.client.connect(); + this.client = new Redis(redisUrl); // Create consumer group (ignore error if already exists) try { - await this.client.xGroupCreate(STREAM_KEY, CONSUMER_GROUP, '0', { MKSTREAM: true }); + await this.client.xgroup('CREATE', STREAM_KEY, CONSUMER_GROUP, '0', 'MKSTREAM'); } catch { // Group already exists } @@ -49,25 +48,29 @@ export class AggregateUsageUseCase implements OnModuleInit, OnModuleDestroy { async onModuleDestroy() { this.running = false; - await this.client.quit(); + this.client.disconnect(); } private async consumeLoop() { while (this.running) { try { - const response = await (this.client as any).xReadGroup( - CONSUMER_GROUP, - CONSUMER_NAME, - [{ key: STREAM_KEY, id: '>' }], - { COUNT: 10, BLOCK: 5000 }, - ); + const response = await this.client.xreadgroup( + 'GROUP', CONSUMER_GROUP, CONSUMER_NAME, + 'COUNT', '10', + 'BLOCK', '5000', + 'STREAMS', STREAM_KEY, '>', + ) as Array<[string, Array<[string, string[]]>]> | null; if (!response) continue; - for (const stream of response) { - for (const message of stream.messages) { - await this.processMessage(message); - await this.client.xAck(STREAM_KEY, CONSUMER_GROUP, message.id); + for (const [, messages] of response) { + for (const [id, fields] of messages) { + const record: Record = {}; + for (let i = 0; i < fields.length; i += 2) { + record[fields[i]] = fields[i + 1]; + } + await this.processMessage(id, record); + await this.client.xack(STREAM_KEY, CONSUMER_GROUP, id); } } } catch (err) { @@ -79,9 +82,9 @@ export class AggregateUsageUseCase implements OnModuleInit, OnModuleDestroy { } } - private async processMessage(message: { id: string; message: Record }) { + private async processMessage(id: string, record: Record) { try { - const payload: UsageRecordedEvent = JSON.parse(message.message.data ?? '{}'); + const payload: UsageRecordedEvent = JSON.parse(record['data'] ?? '{}'); if (!payload.tenantId || !payload.inputTokens) return; const recordedAt = new Date(payload.recordedAt ?? Date.now()); @@ -97,7 +100,7 @@ export class AggregateUsageUseCase implements OnModuleInit, OnModuleDestroy { payload.costUsd ?? 0, ); } catch (err) { - this.logger.error(`Failed to process usage message ${message.id}: ${err.message}`); + this.logger.error(`Failed to process usage message ${id}: ${err.message}`); } } } diff --git a/packages/services/billing-service/src/application/use-cases/change-plan.use-case.ts b/packages/services/billing-service/src/application/use-cases/change-plan.use-case.ts index 6c34b11..cb9c088 100644 --- a/packages/services/billing-service/src/application/use-cases/change-plan.use-case.ts +++ b/packages/services/billing-service/src/application/use-cases/change-plan.use-case.ts @@ -40,7 +40,7 @@ export class ChangePlanUseCase { throw new BadRequestException('Already on this plan'); } - const isUpgrade = newPlan.monthlyPriceUsdCents > currentPlan.monthlyPriceUsdCents; + const isUpgrade = newPlan.monthlyPriceCentsUsd > currentPlan.monthlyPriceCentsUsd; if (isUpgrade) { // Immediate upgrade with proration invoice item @@ -48,7 +48,7 @@ export class ChangePlanUseCase { await this.subscriptionRepo.save(updated); // Generate proration charge if not on free plan - if (newPlan.monthlyPriceUsdCents > 0 && currentPlan.monthlyPriceUsdCents >= 0) { + if (newPlan.monthlyPriceCentsUsd > 0 && currentPlan.monthlyPriceCentsUsd >= 0) { const invoiceNumber = await this.invoiceRepo.getNextInvoiceNumber(); const proratedItem = this.invoiceGenerator.generateProratedUpgradeItem( currentPlan, diff --git a/packages/services/billing-service/src/application/use-cases/check-token-quota.use-case.ts b/packages/services/billing-service/src/application/use-cases/check-token-quota.use-case.ts index 33df31c..11017f8 100644 --- a/packages/services/billing-service/src/application/use-cases/check-token-quota.use-case.ts +++ b/packages/services/billing-service/src/application/use-cases/check-token-quota.use-case.ts @@ -40,7 +40,7 @@ export class CheckTokenQuotaUseCase { ); const usedTokens = usage?.totalTokens ?? 0; - const baseLimit = plan.includedTokensPerMonth; + const baseLimit = plan.includedTokens; // Enterprise: no limit (-1) if (baseLimit === -1) { @@ -55,7 +55,7 @@ export class CheckTokenQuotaUseCase { } const hardLimitTokens = Math.floor(baseLimit * (plan.hardLimitPercent / 100)); - const overageAllowed = plan.overageRateCentsPerMToken > 0; + const overageAllowed = plan.overageRateCentsPerMTokenUsd > 0; // Hard limit check (applies to all plans) if (plan.hardLimitPercent > 0 && usedTokens >= hardLimitTokens) { diff --git a/packages/services/billing-service/src/application/use-cases/create-payment-session.use-case.ts b/packages/services/billing-service/src/application/use-cases/create-payment-session.use-case.ts index 69ddc05..9c3577b 100644 --- a/packages/services/billing-service/src/application/use-cases/create-payment-session.use-case.ts +++ b/packages/services/billing-service/src/application/use-cases/create-payment-session.use-case.ts @@ -42,10 +42,10 @@ export class CreatePaymentSessionUseCase { const session = await paymentProvider.createPaymentSession({ invoiceId, tenantId, - amountCents: invoice.amountDueCents, + amount: invoice.amountDueCents, currency: invoice.currency, description: `IT0 Invoice ${invoice.invoiceNumber}`, - returnUrl, + returnUrl: returnUrl ?? '', }); // Create a pending payment record @@ -57,7 +57,7 @@ export class CreatePaymentSessionUseCase { payment.amountCents = invoice.amountDueCents; payment.currency = invoice.currency; payment.status = PaymentStatus.PENDING; - payment.metadata = { sessionId: session.sessionId }; + payment.metadata = { provider }; await this.paymentRepo.savePayment(payment); diff --git a/packages/services/billing-service/src/application/use-cases/generate-monthly-invoice.use-case.ts b/packages/services/billing-service/src/application/use-cases/generate-monthly-invoice.use-case.ts index 1e69c57..1da6a14 100644 --- a/packages/services/billing-service/src/application/use-cases/generate-monthly-invoice.use-case.ts +++ b/packages/services/billing-service/src/application/use-cases/generate-monthly-invoice.use-case.ts @@ -1,6 +1,4 @@ import { Injectable, Logger } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; -import { DataSource } from 'typeorm'; import { PlanRepository } from '../../infrastructure/repositories/plan.repository'; import { SubscriptionRepository } from '../../infrastructure/repositories/subscription.repository'; import { InvoiceRepository } from '../../infrastructure/repositories/invoice.repository'; @@ -74,7 +72,7 @@ export class GenerateMonthlyInvoiceUseCase { } // Only generate invoice if plan costs money - if (plan.monthlyPriceUsdCents > 0 || (usage && usage.totalTokens > plan.includedTokensPerMonth)) { + if (plan.monthlyPriceCentsUsd > 0 || (usage && usage.totalTokens > plan.includedTokens)) { const invoiceNumber = await this.invoiceRepo.getNextInvoiceNumber(); const { invoice, items } = this.invoiceGenerator.generateMonthlyInvoice( sub, @@ -94,7 +92,7 @@ export class GenerateMonthlyInvoiceUseCase { await this.subscriptionRepo.save(renewed); } else if (sub.status === SubscriptionStatus.TRIALING && sub.trialEndsAt <= new Date()) { // Trial expired: activate if paid plan, else keep on free - if (plan.monthlyPriceUsdCents === 0) { + if (plan.monthlyPriceCentsUsd === 0) { const activated = this.lifecycle.activate(sub); await this.subscriptionRepo.save(activated); } else { @@ -110,13 +108,13 @@ export class GenerateMonthlyInvoiceUseCase { tenantId, year, month, + periodStart: new Date(year, month - 1, 1), + periodEnd: new Date(year, month, 0, 23, 59, 59), totalInputTokens: 0, totalOutputTokens: 0, totalTokens: 0, totalCostUsd: 0, taskCount: 0, - periodStart: new Date(year, month - 1, 1), - periodEnd: new Date(year, month, 0, 23, 59, 59), }; } } diff --git a/packages/services/billing-service/src/application/use-cases/handle-payment-webhook.use-case.ts b/packages/services/billing-service/src/application/use-cases/handle-payment-webhook.use-case.ts index 5a62e17..aba1759 100644 --- a/packages/services/billing-service/src/application/use-cases/handle-payment-webhook.use-case.ts +++ b/packages/services/billing-service/src/application/use-cases/handle-payment-webhook.use-case.ts @@ -58,7 +58,7 @@ export class HandlePaymentWebhookUseCase { await this.paymentRepo.savePayment(payment); // Mark invoice as paid - const invoice = await this.invoiceRepo.findById(payment.invoiceId); + const invoice = await this.invoiceRepo.findById(payment.invoiceId ?? ''); if (invoice) { invoice.status = InvoiceStatus.PAID; invoice.paidAt = new Date(); diff --git a/packages/services/billing-service/src/domain/entities/invoice-item.entity.ts b/packages/services/billing-service/src/domain/entities/invoice-item.entity.ts index 36619e9..4762cc6 100644 --- a/packages/services/billing-service/src/domain/entities/invoice-item.entity.ts +++ b/packages/services/billing-service/src/domain/entities/invoice-item.entity.ts @@ -1,25 +1,36 @@ import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +export const InvoiceItemType = { + SUBSCRIPTION: 'subscription', + OVERAGE: 'overage', + CREDIT: 'credit', + ADJUSTMENT: 'adjustment', +} as const; +export type InvoiceItemType = typeof InvoiceItemType[keyof typeof InvoiceItemType]; + @Entity({ name: 'invoice_items', schema: 'public' }) export class InvoiceItem { @PrimaryGeneratedColumn('uuid') id!: string; - @Column({ type: 'uuid' }) + @Column({ name: 'invoice_id', type: 'uuid' }) invoiceId!: string; @Column({ type: 'varchar', length: 200 }) description!: string; // e.g. 'Pro Plan - Monthly', 'Token Overage (2.5M tokens)' - @Column({ type: 'varchar', length: 30 }) - itemType!: string; // 'subscription' | 'overage' | 'credit' | 'adjustment' + @Column({ name: 'item_type', type: 'varchar', length: 30 }) + itemType!: InvoiceItemType; // 'subscription' | 'overage' | 'credit' | 'adjustment' @Column({ type: 'numeric', precision: 12, scale: 4 }) quantity!: number; // 1 for subscription, 2.5 for 2.5M tokens overage - @Column({ type: 'int' }) + @Column({ name: 'unit_price', type: 'int' }) unitPrice!: number; // in cents/fen @Column({ type: 'int' }) amount!: number; // quantity * unitPrice (rounded) + + @Column({ type: 'varchar', length: 3, default: 'USD' }) + currency!: string; } diff --git a/packages/services/billing-service/src/domain/entities/invoice.entity.ts b/packages/services/billing-service/src/domain/entities/invoice.entity.ts index 13245bf..7308466 100644 --- a/packages/services/billing-service/src/domain/entities/invoice.entity.ts +++ b/packages/services/billing-service/src/domain/entities/invoice.entity.ts @@ -1,19 +1,34 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany } from 'typeorm'; +import { InvoiceItem } from './invoice-item.entity'; -export type InvoiceStatus = 'draft' | 'open' | 'paid' | 'void' | 'uncollectible'; +export const InvoiceStatus = { + DRAFT: 'draft', + OPEN: 'open', + PAID: 'paid', + PAST_DUE: 'past_due', + VOID: 'void', + UNCOLLECTIBLE: 'uncollectible', +} as const; +export type InvoiceStatus = typeof InvoiceStatus[keyof typeof InvoiceStatus]; + +export const InvoiceCurrency = { + USD: 'USD', + CNY: 'CNY', +} as const; +export type InvoiceCurrency = typeof InvoiceCurrency[keyof typeof InvoiceCurrency]; @Entity({ name: 'invoices', schema: 'public' }) export class Invoice { @PrimaryGeneratedColumn('uuid') id!: string; - @Column({ type: 'varchar', length: 100 }) + @Column({ name: 'tenant_id', type: 'varchar', length: 100 }) tenantId!: string; - @Column({ type: 'uuid' }) - subscriptionId!: string; + @Column({ name: 'subscription_id', type: 'uuid', nullable: true }) + subscriptionId?: string; - @Column({ type: 'varchar', length: 30, unique: true }) + @Column({ name: 'invoice_number', type: 'varchar', length: 30, unique: true }) invoiceNumber!: string; // e.g. INV-2026-03-0001 @Column({ type: 'varchar', length: 20, default: 'draft' }) @@ -22,27 +37,33 @@ export class Invoice { @Column({ type: 'varchar', length: 3 }) currency!: string; // 'USD' | 'CNY' - @Column({ type: 'int', default: 0 }) - subtotalAmount!: number; // in cents/fen + @Column({ name: 'subtotal_cents', type: 'int', default: 0 }) + subtotalCents!: number; // in cents/fen - @Column({ type: 'int', default: 0 }) - taxAmount!: number; + @Column({ name: 'tax_cents', type: 'int', default: 0 }) + taxCents!: number; - @Column({ type: 'int', default: 0 }) - totalAmount!: number; + @Column({ name: 'total_cents', type: 'int', default: 0 }) + totalCents!: number; - @Column({ type: 'timestamptz' }) + @Column({ name: 'amount_due_cents', type: 'int', default: 0 }) + amountDueCents!: number; + + @Column({ name: 'period_start', type: 'timestamptz' }) periodStart!: Date; - @Column({ type: 'timestamptz' }) + @Column({ name: 'period_end', type: 'timestamptz' }) periodEnd!: Date; - @Column({ type: 'timestamptz' }) + @Column({ name: 'due_date', type: 'timestamptz' }) dueDate!: Date; - @Column({ type: 'timestamptz', nullable: true }) + @Column({ name: 'paid_at', type: 'timestamptz', nullable: true }) paidAt?: Date; + @OneToMany(() => InvoiceItem, (item) => item.invoiceId, { eager: false }) + items?: InvoiceItem[]; + @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; } diff --git a/packages/services/billing-service/src/domain/entities/payment-method.entity.ts b/packages/services/billing-service/src/domain/entities/payment-method.entity.ts index ce5c8bf..6035c28 100644 --- a/packages/services/billing-service/src/domain/entities/payment-method.entity.ts +++ b/packages/services/billing-service/src/domain/entities/payment-method.entity.ts @@ -1,28 +1,37 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; @Entity({ name: 'payment_methods', schema: 'public' }) export class PaymentMethod { @PrimaryGeneratedColumn('uuid') id!: string; - @Column({ type: 'varchar', length: 100 }) + @Column({ name: 'tenant_id', type: 'varchar', length: 100 }) tenantId!: string; @Column({ type: 'varchar', length: 20 }) provider!: string; // 'stripe' | 'alipay' | 'wechat_pay' | 'crypto' - @Column({ type: 'varchar', length: 30 }) - type!: string; // 'card' | 'alipay_account' | 'wechat_openid' | 'crypto_wallet' + @Column({ name: 'display_name', type: 'varchar', length: 200, default: '' }) + displayName!: string; // e.g. 'Visa *4242', 'Alipay xxx@qq.com' - @Column({ type: 'boolean', default: false }) + @Column({ type: 'varchar', length: 30, nullable: true }) + type?: string; // 'card' | 'alipay_account' | 'wechat_openid' | 'crypto_wallet' + + @Column({ name: 'is_default', type: 'boolean', default: false }) isDefault!: boolean; @Column({ type: 'jsonb', default: '{}' }) details!: Record; // { last4: '4242', brand: 'visa' } — NEVER store full card numbers - @Column({ type: 'varchar', length: 200, nullable: true }) + @Column({ name: 'provider_customer_id', type: 'varchar', length: 200, nullable: true }) providerCustomerId?: string; + @Column({ name: 'expires_at', type: 'timestamptz', nullable: true }) + expiresAt?: Date; + @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: Date; } diff --git a/packages/services/billing-service/src/domain/entities/payment.entity.ts b/packages/services/billing-service/src/domain/entities/payment.entity.ts index 0e26461..ada5961 100644 --- a/packages/services/billing-service/src/domain/entities/payment.entity.ts +++ b/packages/services/billing-service/src/domain/entities/payment.entity.ts @@ -1,6 +1,13 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; -export type PaymentStatus = 'pending' | 'processing' | 'succeeded' | 'failed' | 'refunded'; +export const PaymentStatus = { + PENDING: 'pending', + PROCESSING: 'processing', + SUCCEEDED: 'succeeded', + FAILED: 'failed', + REFUNDED: 'refunded', +} as const; +export type PaymentStatus = typeof PaymentStatus[keyof typeof PaymentStatus]; @Entity({ name: 'payments', schema: 'public' }) export class Payment { @@ -16,11 +23,11 @@ export class Payment { @Column({ type: 'varchar', length: 20 }) provider!: string; // 'stripe' | 'alipay' | 'wechat_pay' | 'crypto' - @Column({ type: 'varchar', length: 200 }) + @Column({ name: 'provider_payment_id', type: 'varchar', length: 200 }) providerPaymentId!: string; - @Column({ type: 'int' }) - amount!: number; // in cents/fen + @Column({ name: 'amount_cents', type: 'int' }) + amountCents!: number; // in cents/fen @Column({ type: 'varchar', length: 3 }) currency!: string; @@ -28,6 +35,9 @@ export class Payment { @Column({ type: 'varchar', length: 20, default: 'pending' }) status!: PaymentStatus; + @Column({ name: 'paid_at', type: 'timestamptz', nullable: true }) + paidAt?: Date; + @Column({ type: 'jsonb', default: '{}' }) metadata!: Record; diff --git a/packages/services/billing-service/src/domain/entities/subscription.entity.ts b/packages/services/billing-service/src/domain/entities/subscription.entity.ts index bc156ea..ca40774 100644 --- a/packages/services/billing-service/src/domain/entities/subscription.entity.ts +++ b/packages/services/billing-service/src/domain/entities/subscription.entity.ts @@ -1,6 +1,13 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; -export type SubscriptionStatus = 'trialing' | 'active' | 'past_due' | 'cancelled' | 'expired'; +export const SubscriptionStatus = { + TRIALING: 'trialing', + ACTIVE: 'active', + PAST_DUE: 'past_due', + CANCELLED: 'cancelled', + EXPIRED: 'expired', +} as const; +export type SubscriptionStatus = typeof SubscriptionStatus[keyof typeof SubscriptionStatus]; @Entity({ name: 'subscriptions', schema: 'public' }) export class Subscription { diff --git a/packages/services/billing-service/src/domain/entities/usage-aggregate.entity.ts b/packages/services/billing-service/src/domain/entities/usage-aggregate.entity.ts index c7b3904..312bd9f 100644 --- a/packages/services/billing-service/src/domain/entities/usage-aggregate.entity.ts +++ b/packages/services/billing-service/src/domain/entities/usage-aggregate.entity.ts @@ -5,28 +5,34 @@ export class UsageAggregate { @PrimaryGeneratedColumn('uuid') id!: string; - @Column({ type: 'varchar', length: 100 }) + @Column({ name: 'tenant_id', type: 'varchar', length: 100 }) tenantId!: string; @Column({ type: 'int' }) - periodYear!: number; + year!: number; @Column({ type: 'int' }) - periodMonth!: number; + month!: number; - @Column({ type: 'bigint', default: 0 }) + @Column({ name: 'period_start', type: 'timestamptz', nullable: true }) + periodStart?: Date; + + @Column({ name: 'period_end', type: 'timestamptz', nullable: true }) + periodEnd?: Date; + + @Column({ name: 'total_input_tokens', type: 'bigint', default: 0 }) totalInputTokens!: number; - @Column({ type: 'bigint', default: 0 }) + @Column({ name: 'total_output_tokens', type: 'bigint', default: 0 }) totalOutputTokens!: number; - @Column({ type: 'bigint', default: 0 }) + @Column({ name: 'total_tokens', type: 'bigint', default: 0 }) totalTokens!: number; - @Column({ type: 'numeric', precision: 14, scale: 6, default: 0 }) + @Column({ name: 'total_cost_usd', type: 'numeric', precision: 14, scale: 6, default: 0 }) totalCostUsd!: number; - @Column({ type: 'int', default: 0 }) + @Column({ name: 'task_count', type: 'int', default: 0 }) taskCount!: number; @UpdateDateColumn({ type: 'timestamptz' }) diff --git a/packages/services/billing-service/src/domain/ports/payment-provider.port.ts b/packages/services/billing-service/src/domain/ports/payment-provider.port.ts index 0f38664..79a31db 100644 --- a/packages/services/billing-service/src/domain/ports/payment-provider.port.ts +++ b/packages/services/billing-service/src/domain/ports/payment-provider.port.ts @@ -1,4 +1,10 @@ -export type PaymentProviderType = 'stripe' | 'alipay' | 'wechat_pay' | 'crypto'; +export const PaymentProviderType = { + STRIPE: 'stripe' as const, + ALIPAY: 'alipay' as const, + WECHAT_PAY: 'wechat_pay' as const, + CRYPTO: 'crypto' as const, +}; +export type PaymentProviderType = typeof PaymentProviderType[keyof typeof PaymentProviderType]; export interface CreatePaymentParams { amount: number; // in cents/fen @@ -11,6 +17,9 @@ export interface CreatePaymentParams { metadata?: Record; } +// Alias for backwards compatibility +export type PaymentSessionRequest = CreatePaymentParams; + export interface PaymentSession { providerPaymentId: string; redirectUrl?: string; // Alipay, WeChat, Crypto @@ -28,17 +37,20 @@ export interface PaymentResult { paidAt?: Date; } -export interface WebhookEvent { - eventType: 'payment_succeeded' | 'payment_failed' | 'refund_completed'; - providerPaymentId: string; - amount: number; - currency: string; - metadata: Record; +export interface WebhookResult { + type: 'payment_succeeded' | 'payment_failed' | 'refunded' | 'unknown'; + providerPaymentId?: string; + amount?: number; + currency?: string; + metadata?: Record; } +// Alias for backwards compatibility +export type WebhookEvent = WebhookResult; + export interface PaymentProviderPort { readonly providerType: PaymentProviderType; createPaymentSession(params: CreatePaymentParams): Promise; confirmPayment(providerPaymentId: string): Promise; - handleWebhook(payload: Buffer, headers: Record): Promise; + handleWebhook(payload: Buffer, headers: Record): Promise; } diff --git a/packages/services/billing-service/src/domain/services/invoice-generator.service.ts b/packages/services/billing-service/src/domain/services/invoice-generator.service.ts index b012e9a..ac66d0a 100644 --- a/packages/services/billing-service/src/domain/services/invoice-generator.service.ts +++ b/packages/services/billing-service/src/domain/services/invoice-generator.service.ts @@ -23,20 +23,20 @@ export class InvoiceGeneratorService { invoice.invoiceNumber = invoiceNumber; invoice.status = InvoiceStatus.OPEN; invoice.currency = currency; - invoice.periodStart = usage.periodStart; - invoice.periodEnd = usage.periodEnd; + invoice.periodStart = usage.periodStart ?? new Date(usage.year, usage.month - 1, 1); + invoice.periodEnd = usage.periodEnd ?? new Date(usage.year, usage.month, 0, 23, 59, 59); invoice.dueDate = this.addDays(new Date(), 14); const items: InvoiceItem[] = []; // 1. Base subscription fee const baseAmountCents = - currency === InvoiceCurrency.CNY ? plan.monthlyPriceCny : plan.monthlyPriceUsdCents; + currency === InvoiceCurrency.CNY ? plan.monthlyPriceFenCny : plan.monthlyPriceCentsUsd; if (baseAmountCents > 0) { const subItem = new InvoiceItem(); subItem.itemType = InvoiceItemType.SUBSCRIPTION; - subItem.description = `${plan.displayName} subscription (${this.formatPeriod(usage.periodStart, usage.periodEnd)})`; + subItem.description = `${plan.displayName} subscription (${this.formatPeriod(invoice.periodStart, invoice.periodEnd)})`; subItem.quantity = 1; subItem.unitPrice = baseAmountCents; subItem.amount = baseAmountCents; @@ -45,20 +45,20 @@ export class InvoiceGeneratorService { } // 2. Overage charges (token usage beyond plan quota) - if (plan.overageRateCentsPerMToken > 0 && usage.totalTokens > plan.includedTokensPerMonth) { + if (plan.overageRateCentsPerMTokenUsd > 0 && usage.totalTokens > plan.includedTokens) { const overageResult = this.overageCalculator.calculate( usage.totalTokens, - plan.includedTokensPerMonth, - plan.overageRateCentsPerMToken, + plan.includedTokens, + plan.overageRateCentsPerMTokenUsd, ); - if (overageResult.overageAmountCents > 0) { + if (overageResult.overageCentsUsd > 0) { const overageItem = new InvoiceItem(); overageItem.itemType = InvoiceItemType.OVERAGE; - overageItem.description = `Token overage: ${this.formatTokens(overageResult.overageTokens)} tokens × ${this.formatRate(plan.overageRateCentsPerMToken, currency)}/MTok`; + overageItem.description = `Token overage: ${this.formatTokens(overageResult.overageTokens)} tokens × ${this.formatRate(plan.overageRateCentsPerMTokenUsd, currency)}/MTok`; overageItem.quantity = overageResult.overageTokens; - overageItem.unitPrice = plan.overageRateCentsPerMToken; // per million tokens - overageItem.amount = overageResult.overageAmountCents; + overageItem.unitPrice = plan.overageRateCentsPerMTokenUsd; // per million tokens + overageItem.amount = overageResult.overageCentsUsd; overageItem.currency = currency; items.push(overageItem); } @@ -92,8 +92,8 @@ export class InvoiceGeneratorService { const remainingDays = this.daysBetween(upgradeDate, periodEnd); const fraction = remainingDays / totalDays; - const oldPrice = currency === InvoiceCurrency.CNY ? oldPlan.monthlyPriceCny : oldPlan.monthlyPriceUsdCents; - const newPrice = currency === InvoiceCurrency.CNY ? newPlan.monthlyPriceCny : newPlan.monthlyPriceUsdCents; + const oldPrice = currency === InvoiceCurrency.CNY ? oldPlan.monthlyPriceFenCny : oldPlan.monthlyPriceCentsUsd; + const newPrice = currency === InvoiceCurrency.CNY ? newPlan.monthlyPriceFenCny : newPlan.monthlyPriceCentsUsd; const proratedAmount = Math.round((newPrice - oldPrice) * fraction); const item = new InvoiceItem(); @@ -117,7 +117,7 @@ export class InvoiceGeneratorService { return Math.ceil(ms / (1000 * 60 * 60 * 24)); } - private formatPeriod(start: Date, end: Date): string { + private formatPeriod(start: Date, _end: Date): string { return `${start.toISOString().slice(0, 7)}`; } diff --git a/packages/services/billing-service/src/infrastructure/payment-providers/alipay/alipay.provider.ts b/packages/services/billing-service/src/infrastructure/payment-providers/alipay/alipay.provider.ts index 4f60a6d..20a7954 100644 --- a/packages/services/billing-service/src/infrastructure/payment-providers/alipay/alipay.provider.ts +++ b/packages/services/billing-service/src/infrastructure/payment-providers/alipay/alipay.provider.ts @@ -3,8 +3,9 @@ import { ConfigService } from '@nestjs/config'; import { PaymentProviderPort, PaymentProviderType, - PaymentSessionRequest, + CreatePaymentParams, PaymentSession, + PaymentResult, WebhookResult, } from '../../../domain/ports/payment-provider.port'; import { InvoiceCurrency } from '../../../domain/entities/invoice.entity'; @@ -27,17 +28,18 @@ export class AlipayProvider implements PaymentProviderPort { return this.configured; } - async createPaymentSession(req: PaymentSessionRequest): Promise { + async createPaymentSession(params: CreatePaymentParams): Promise { if (!this.configured) throw new Error('Alipay not configured'); // For CNY: use amount directly; for USD: this should not happen but handle gracefully - if (req.currency !== InvoiceCurrency.CNY) { + if (params.currency !== InvoiceCurrency.CNY) { throw new Error('Alipay only supports CNY payments'); } // Dynamic import to avoid load errors when alipay-sdk is not installed - const AlipaySdk = (await import('alipay-sdk')).default; - const client = new AlipaySdk({ + const AlipaySdkModule = await import('alipay-sdk'); + const AlipaySdk = AlipaySdkModule.default ?? AlipaySdkModule; + const client = new (AlipaySdk as any)({ appId: this.appId, privateKey: this.privateKey, signType: 'RSA2', @@ -45,29 +47,31 @@ export class AlipayProvider implements PaymentProviderPort { }); // alipay amount is in yuan (CNY fen / 100) - const amountYuan = (req.amountCents / 100).toFixed(2); - const outTradeNo = `it0-${req.invoiceId}-${Date.now()}`; + const amountYuan = (params.amount / 100).toFixed(2); + const outTradeNo = `it0-${params.invoiceId}-${Date.now()}`; // PC scan QR code payment const result = await client.exec('alipay.trade.precreate', { bizContent: { out_trade_no: outTradeNo, total_amount: amountYuan, - subject: req.description, + subject: params.description, }, }); return { - sessionId: outTradeNo, providerPaymentId: outTradeNo, qrCodeUrl: result.qrCode, - redirectUrl: undefined, - clientSecret: undefined, expiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000), // 2 hours }; } - async handleWebhook(rawBody: Buffer, headers: Record): Promise { + async confirmPayment(providerPaymentId: string): Promise { + // Alipay uses async webhook notification; sync confirm not needed + return { providerPaymentId, status: 'pending', amount: 0, currency: 'CNY' }; + } + + async handleWebhook(rawBody: Buffer, _headers: Record): Promise { if (!this.configured) throw new Error('Alipay not configured'); // Parse URL-encoded body @@ -76,8 +80,9 @@ export class AlipayProvider implements PaymentProviderPort { params.forEach((v, k) => { notifyData[k] = v; }); // Verify Alipay signature using alipay-sdk - const AlipaySdk = (await import('alipay-sdk')).default; - const client = new AlipaySdk({ + const AlipaySdkModule = await import('alipay-sdk'); + const AlipaySdk = AlipaySdkModule.default ?? AlipaySdkModule; + const client = new (AlipaySdk as any)({ appId: this.appId, privateKey: this.privateKey, signType: 'RSA2', diff --git a/packages/services/billing-service/src/infrastructure/payment-providers/crypto/crypto.provider.ts b/packages/services/billing-service/src/infrastructure/payment-providers/crypto/crypto.provider.ts index 29915f1..60785e6 100644 --- a/packages/services/billing-service/src/infrastructure/payment-providers/crypto/crypto.provider.ts +++ b/packages/services/billing-service/src/infrastructure/payment-providers/crypto/crypto.provider.ts @@ -4,8 +4,9 @@ import * as crypto from 'crypto'; import { PaymentProviderPort, PaymentProviderType, - PaymentSessionRequest, + CreatePaymentParams, PaymentSession, + PaymentResult, WebhookResult, } from '../../../domain/ports/payment-provider.port'; @@ -29,11 +30,11 @@ export class CryptoProvider implements PaymentProviderPort { return this.configured; } - async createPaymentSession(req: PaymentSessionRequest): Promise { + async createPaymentSession(params: CreatePaymentParams): Promise { if (!this.configured) throw new Error('Coinbase Commerce not configured'); // Convert cents to dollar amount - const amount = (req.amountCents / 100).toFixed(2); + const amount = (params.amount / 100).toFixed(2); const response = await fetch(`${CryptoProvider.BASE_URL}/charges`, { method: 'POST', @@ -44,14 +45,14 @@ export class CryptoProvider implements PaymentProviderPort { }, body: JSON.stringify({ name: 'IT0 Platform', - description: req.description, + description: params.description, local_price: { amount, currency: 'USD' }, pricing_type: 'fixed_price', metadata: { - invoiceId: req.invoiceId, - tenantId: req.tenantId, + invoiceId: params.invoiceId, + tenantId: params.tenantId, }, - redirect_url: req.returnUrl, + redirect_url: params.returnUrl, }), }); @@ -64,15 +65,17 @@ export class CryptoProvider implements PaymentProviderPort { const charge = data.data; return { - sessionId: charge.id, providerPaymentId: charge.id, redirectUrl: charge.hosted_url, - qrCodeUrl: undefined, - clientSecret: undefined, expiresAt: new Date(charge.expires_at), }; } + async confirmPayment(providerPaymentId: string): Promise { + // Coinbase Commerce uses async webhook notification + return { providerPaymentId, status: 'pending', amount: 0, currency: 'USD' }; + } + async handleWebhook(rawBody: Buffer, headers: Record): Promise { if (!this.configured) throw new Error('Coinbase Commerce not configured'); diff --git a/packages/services/billing-service/src/infrastructure/payment-providers/stripe/stripe.provider.ts b/packages/services/billing-service/src/infrastructure/payment-providers/stripe/stripe.provider.ts index 05651ed..a0ac7d6 100644 --- a/packages/services/billing-service/src/infrastructure/payment-providers/stripe/stripe.provider.ts +++ b/packages/services/billing-service/src/infrastructure/payment-providers/stripe/stripe.provider.ts @@ -4,8 +4,9 @@ import Stripe from 'stripe'; import { PaymentProviderPort, PaymentProviderType, - PaymentSessionRequest, + CreatePaymentParams, PaymentSession, + PaymentResult, WebhookResult, } from '../../../domain/ports/payment-provider.port'; @@ -19,7 +20,7 @@ export class StripeProvider implements PaymentProviderPort { const secretKey = this.configService.get('STRIPE_SECRET_KEY'); this.webhookSecret = this.configService.get('STRIPE_WEBHOOK_SECRET', ''); if (secretKey) { - this.client = new Stripe(secretKey, { apiVersion: '2024-11-20.acacia' }); + this.client = new Stripe(secretKey, { apiVersion: '2023-10-16' }); } } @@ -27,29 +28,37 @@ export class StripeProvider implements PaymentProviderPort { return !!this.client; } - async createPaymentSession(req: PaymentSessionRequest): Promise { + async createPaymentSession(params: CreatePaymentParams): Promise { if (!this.client) throw new Error('Stripe not configured'); const intent = await this.client.paymentIntents.create({ - amount: req.amountCents, - currency: req.currency.toLowerCase(), + amount: params.amount, + currency: params.currency.toLowerCase(), metadata: { - invoiceId: req.invoiceId, - tenantId: req.tenantId, + invoiceId: params.invoiceId, + tenantId: params.tenantId, }, - description: req.description, + description: params.description, }); return { - sessionId: intent.id, providerPaymentId: intent.id, clientSecret: intent.client_secret ?? undefined, - redirectUrl: undefined, - qrCodeUrl: undefined, expiresAt: new Date(Date.now() + 30 * 60 * 1000), // 30 minutes }; } + async confirmPayment(providerPaymentId: string): Promise { + if (!this.client) throw new Error('Stripe not configured'); + const intent = await this.client.paymentIntents.retrieve(providerPaymentId); + return { + providerPaymentId: intent.id, + status: intent.status === 'succeeded' ? 'succeeded' : intent.status === 'canceled' ? 'failed' : 'pending', + amount: intent.amount, + currency: intent.currency.toUpperCase(), + }; + } + async handleWebhook(rawBody: Buffer, headers: Record): Promise { if (!this.client) throw new Error('Stripe not configured'); diff --git a/packages/services/billing-service/src/infrastructure/payment-providers/wechat/wechat-pay.provider.ts b/packages/services/billing-service/src/infrastructure/payment-providers/wechat/wechat-pay.provider.ts index dbd3bec..1987edc 100644 --- a/packages/services/billing-service/src/infrastructure/payment-providers/wechat/wechat-pay.provider.ts +++ b/packages/services/billing-service/src/infrastructure/payment-providers/wechat/wechat-pay.provider.ts @@ -4,8 +4,9 @@ import * as crypto from 'crypto'; import { PaymentProviderPort, PaymentProviderType, - PaymentSessionRequest, + CreatePaymentParams, PaymentSession, + PaymentResult, WebhookResult, } from '../../../domain/ports/payment-provider.port'; import { InvoiceCurrency } from '../../../domain/entities/invoice.entity'; @@ -33,14 +34,14 @@ export class WeChatPayProvider implements PaymentProviderPort { return this.configured; } - async createPaymentSession(req: PaymentSessionRequest): Promise { + async createPaymentSession(params: CreatePaymentParams): Promise { if (!this.configured) throw new Error('WeChat Pay not configured'); - if (req.currency !== InvoiceCurrency.CNY) { + if (params.currency !== InvoiceCurrency.CNY) { throw new Error('WeChat Pay only supports CNY payments'); } - const outTradeNo = `it0-${req.invoiceId}-${Date.now()}`; + const outTradeNo = `it0-${params.invoiceId}-${Date.now()}`; const notifyUrl = this.configService.get( 'WECHAT_NOTIFY_URL', 'https://it0api.szaiai.com/api/v1/billing/webhooks/wechat', @@ -49,25 +50,27 @@ export class WeChatPayProvider implements PaymentProviderPort { const body = { appid: this.appId, mchid: this.mchId, - description: req.description, + description: params.description, out_trade_no: outTradeNo, notify_url: notifyUrl, - amount: { total: req.amountCents, currency: 'CNY' }, + amount: { total: params.amount, currency: 'CNY' }, }; const response = await this.request('POST', '/v3/pay/transactions/native', body); const qrCodeUrl = response.code_url; return { - sessionId: outTradeNo, providerPaymentId: outTradeNo, qrCodeUrl, - redirectUrl: undefined, - clientSecret: undefined, expiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000), }; } + async confirmPayment(providerPaymentId: string): Promise { + // WeChat Pay uses async webhook notification; sync confirm not needed + return { providerPaymentId, status: 'pending', amount: 0, currency: 'CNY' }; + } + async handleWebhook(rawBody: Buffer, headers: Record): Promise { if (!this.configured) throw new Error('WeChat Pay not configured'); diff --git a/packages/services/billing-service/src/infrastructure/repositories/plan.repository.ts b/packages/services/billing-service/src/infrastructure/repositories/plan.repository.ts index 7646d42..01046a4 100644 --- a/packages/services/billing-service/src/infrastructure/repositories/plan.repository.ts +++ b/packages/services/billing-service/src/infrastructure/repositories/plan.repository.ts @@ -11,7 +11,7 @@ export class PlanRepository { } async findAll(): Promise { - return this.repo.find({ where: { isActive: true }, order: { monthlyPriceUsdCents: 'ASC' } }); + return this.repo.find({ where: { isActive: true }, order: { monthlyPriceCentsUsd: 'ASC' } }); } async findById(id: string): Promise { diff --git a/packages/services/billing-service/src/infrastructure/repositories/usage-aggregate.repository.ts b/packages/services/billing-service/src/infrastructure/repositories/usage-aggregate.repository.ts index 80152ec..b529f3d 100644 --- a/packages/services/billing-service/src/infrastructure/repositories/usage-aggregate.repository.ts +++ b/packages/services/billing-service/src/infrastructure/repositories/usage-aggregate.repository.ts @@ -22,7 +22,7 @@ export class UsageAggregateRepository { async findHistoryByTenantId(tenantId: string, limit = 12): Promise { return this.repo.find({ where: { tenantId }, - order: { year: 'DESC', month: 'DESC' }, + order: { year: 'DESC', month: 'DESC' } as any, take: limit, }); } @@ -43,9 +43,9 @@ export class UsageAggregateRepository { INSERT INTO billing_usage_aggregates (id, tenant_id, year, month, period_start, period_end, total_input_tokens, total_output_tokens, total_tokens, - total_cost_usd, task_count, created_at, updated_at) + total_cost_usd, task_count, updated_at) VALUES - (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, 1, NOW(), NOW()) + (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, 1, NOW()) ON CONFLICT (tenant_id, year, month) DO UPDATE SET total_input_tokens = billing_usage_aggregates.total_input_tokens + EXCLUDED.total_input_tokens, diff --git a/packages/services/billing-service/src/interfaces/rest/controllers/plan.controller.ts b/packages/services/billing-service/src/interfaces/rest/controllers/plan.controller.ts index b1a506a..b8f2b7e 100644 --- a/packages/services/billing-service/src/interfaces/rest/controllers/plan.controller.ts +++ b/packages/services/billing-service/src/interfaces/rest/controllers/plan.controller.ts @@ -12,10 +12,10 @@ export class PlanController { id: p.id, name: p.name, displayName: p.displayName, - monthlyPriceUsd: p.monthlyPriceUsdCents / 100, - monthlyPriceCny: p.monthlyPriceCny / 100, - includedTokensPerMonth: p.includedTokensPerMonth, - overageRateUsdPerMToken: p.overageRateCentsPerMToken / 100, + monthlyPriceUsd: p.monthlyPriceCentsUsd / 100, + monthlyPriceCny: p.monthlyPriceFenCny / 100, + includedTokensPerMonth: p.includedTokens, + overageRateUsdPerMToken: p.overageRateCentsPerMTokenUsd / 100, maxServers: p.maxServers, maxUsers: p.maxUsers, maxStandingOrders: p.maxStandingOrders, diff --git a/packages/services/billing-service/src/interfaces/rest/controllers/webhook.controller.ts b/packages/services/billing-service/src/interfaces/rest/controllers/webhook.controller.ts index 1aed59f..92d0c69 100644 --- a/packages/services/billing-service/src/interfaces/rest/controllers/webhook.controller.ts +++ b/packages/services/billing-service/src/interfaces/rest/controllers/webhook.controller.ts @@ -1,7 +1,6 @@ import { Controller, Post, Req, Headers, HttpCode, HttpStatus, Logger, BadRequestException, } from '@nestjs/common'; -import { Request } from 'express'; import { HandlePaymentWebhookUseCase } from '../../../application/use-cases/handle-payment-webhook.use-case'; import { PaymentProviderType } from '../../../domain/ports/payment-provider.port'; @@ -13,13 +12,13 @@ export class WebhookController { @Post('stripe') @HttpCode(HttpStatus.OK) - async stripeWebhook(@Req() req: Request, @Headers() headers: Record) { + async stripeWebhook(@Req() req: any, @Headers() headers: Record) { return this.processWebhook(PaymentProviderType.STRIPE, req, headers); } @Post('alipay') @HttpCode(HttpStatus.OK) - async alipayWebhook(@Req() req: Request, @Headers() headers: Record) { + async alipayWebhook(@Req() req: any, @Headers() headers: Record) { await this.processWebhook(PaymentProviderType.ALIPAY, req, headers); // Alipay expects plain text "success" response return 'success'; @@ -27,23 +26,23 @@ export class WebhookController { @Post('wechat') @HttpCode(HttpStatus.OK) - async wechatWebhook(@Req() req: Request, @Headers() headers: Record) { + async wechatWebhook(@Req() req: any, @Headers() headers: Record) { await this.processWebhook(PaymentProviderType.WECHAT_PAY, req, headers); return { code: 'SUCCESS', message: '成功' }; } @Post('crypto') @HttpCode(HttpStatus.OK) - async cryptoWebhook(@Req() req: Request, @Headers() headers: Record) { + async cryptoWebhook(@Req() req: any, @Headers() headers: Record) { return this.processWebhook(PaymentProviderType.CRYPTO, req, headers); } private async processWebhook( provider: PaymentProviderType, - req: Request, + req: any, headers: Record, ) { - const rawBody = (req as any).rawBody as Buffer; + const rawBody = req.rawBody as Buffer; if (!rawBody) { throw new BadRequestException('Raw body not available — ensure rawBody middleware is enabled'); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c37822b..2aac6c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,6 +305,9 @@ importers: coinbase-commerce-node: specifier: ^1.0.4 version: 1.0.4 + ioredis: + specifier: ^5.3.0 + version: 5.9.2 pg: specifier: ^8.11.0 version: 8.18.0