All files / src/modules/billing billing.service.ts

90.69% Statements 39/43
83.33% Branches 20/24
90.9% Functions 10/11
97.14% Lines 34/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 12113x 13x     13x 23x     1x       2x 2x   1x 1x             2x         1x 1x 1x                 3x   3x                       3x         1x           1x 1x 1x   1x 1x             2x 1x   1x   1x                     1x 1x           1x             3x 2x 2x 1x 1x                    
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@app/modules/prisma/prisma.service';
 
@Injectable()
export class BillingService {
  constructor(private readonly prisma: PrismaService) {}
 
  listPlans() {
    return this.prisma.client.plan.findMany({ orderBy: [{ scope: 'asc' }, { period: 'asc' }] as any });
  }
 
  async subscribe(userId: number, dto: { planCode: string; period?: string; paymentMethodId?: string }) {
    const plan = await this.prisma.client.plan.findUnique({ where: { code: dto.planCode } });
    if (!plan) throw new Error('Unknown plan');
    // Placeholder: create subscription in provider (Stripe later). For now, mark active until +30d.
    const currentPeriodEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
    return this.prisma.client.subscription.create({
      data: { userId, planId: plan.id, status: 'ACTIVE', currentPeriodEnd, provider: 'stripe' },
    });
  }
 
  listInvoices(userId: number) {
    // Future: real invoices table; for now, map payments
    return this.prisma.client.payment.findMany({ where: { userId }, orderBy: { createdAt: 'desc' } });
  }
 
  async getInvoice(id: number, userId: number) {
    // @ts-ignore
    const p = await this.prisma.client.payment.findUnique({ where: { id } });
    Iif (!p || p.userId !== userId) throw new Error('Invoice not found');
    return p;
  }
 
  handleStripeWebhook(_req: any, _sig: string) {
    // Placeholder: verify signature and update subscriptions/payments
    return { received: true };
  }
 
  async createPaymentIntent(userId: number, body: { amountCents: number; currency?: string; purpose: string; subjectRef?: string }) {
    const merchantTransId = `p_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
    // @ts-ignore
    const payment = await this.prisma.client.payment.create({
      data: {
        userId,
        amountCents: body.amountCents,
        currency: body.currency || 'UZS',
        purpose: body.purpose,
        status: 'NEW',
        provider: 'click',
        merchantTransId,
        meta: body.subjectRef ? ({ subjectRef: body.subjectRef } as any) : undefined,
      },
    });
    return { merchantTransId: payment.merchantTransId, amountCents: payment.amountCents, purpose: payment.purpose };
  }
 
  getPaymentByMerchantTransId(tid: string) {
    // @ts-ignore
    return this.prisma.client.payment.findUnique({ where: { merchantTransId: tid } });
  }
 
  async requestRefund(merchantTransId: string, userId: number) {
    // Simplified: mark as REFUND_REQUESTED if owned and confirmed
    // @ts-ignore
    const payment = await this.prisma.client.payment.findUnique({ where: { merchantTransId } });
    Iif (!payment || payment.userId !== userId) throw new Error('Payment not found');
    Iif (payment.status !== 'CONFIRMED') throw new Error('Only confirmed payments can be refunded');
    // @ts-ignore
    await this.prisma.client.payment.update({ where: { id: payment.id }, data: { status: 'REFUND_REQUESTED' } });
    return { ok: true };
  }
 
  // ----------------------------
  // Seat subscriptions (AGENT_SEAT)
  // ----------------------------
  async createSeatSubscription(agencyId: number, seatCount: number) {
    if (!agencyId || !Number.isInteger(seatCount) || seatCount <= 0) {
      throw new Error('Invalid parameters');
    }
    const currentPeriodEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
    // @ts-ignore
    const sub = await this.prisma.client.subscription.create({
      data: {
        agencyId,
        type: 'AGENT_SEAT',
        quantity: seatCount,
        status: 'PAST_DUE',
        currentPeriodEnd,
        provider: 'click',
      },
    });
    // Initiate payment intent: 50_000 UZS per seat (amountCents is in UZS cents or integer minor unit)
    const amountCents = 50000 * seatCount;
    const intent = await this.createPaymentIntent(0, {
      amountCents,
      currency: 'UZS',
      purpose: 'AGENT_SEAT',
      subjectRef: JSON.stringify({ agencyId, subscriptionId: sub.id }),
    });
    return { subscriptionId: sub.id, merchantTransId: intent.merchantTransId, amountCents: intent.amountCents };
  }
 
  // ----------------------------
  // Pay-to-publish one-time fee
  // ----------------------------
  async createPayToPublishIntent(userId: number, listingId: number) {
    if (!userId || !listingId) throw new Error('Invalid parameters');
    const listing = await (this.prisma.client as any).listing.findUnique({ where: { id: listingId } });
    if (!listing) throw new Error('Listing not found');
    const amountCents = 25000;
    return this.createPaymentIntent(userId, {
      amountCents,
      currency: 'UZS',
      purpose: 'LISTING_FEE',
      subjectRef: JSON.stringify({ listingId }),
    });
  }
}