Add billing-finance-core service
This commit is contained in:
parent
a6d1b24c01
commit
d5d89258c3
58 changed files with 1754 additions and 0 deletions
9
billing-finance-core/.env.example
Normal file
9
billing-finance-core/.env.example
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
NODE_ENV=development
|
||||
PORT=3000
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/billing_finance_core
|
||||
JWT_SECRET=change-me
|
||||
JWT_PUBLIC_KEY=
|
||||
JWT_ISSUER=identity-gateway
|
||||
PAYMENT_WEBHOOK_SECRET=change-me
|
||||
STRIPE_API_KEY=
|
||||
APP_LOG_LEVEL=info
|
||||
18
billing-finance-core/Dockerfile
Normal file
18
billing-finance-core/Dockerfile
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
COPY tsconfig.json nest-cli.json ./
|
||||
COPY prisma ./prisma
|
||||
COPY src ./src
|
||||
RUN npm run prisma:generate
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install --omit=dev
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
CMD ["node", "dist/main.js"]
|
||||
77
billing-finance-core/README.md
Normal file
77
billing-finance-core/README.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# billing-finance-core
|
||||
|
||||
Backend financeiro, billing, fiscal e CRM para uma plataforma SaaS multi-tenant.
|
||||
|
||||
## Visão geral
|
||||
- **Multi-tenant desde o início** com isolamento por `tenantId`.
|
||||
- **Sem autenticação própria**: confia no `identity-gateway` via JWT interno.
|
||||
- **Core financeiro**: planos, assinaturas, invoices, pagamentos, conciliação.
|
||||
- **Fiscal (base)**: estrutura para emissão de NFS-e.
|
||||
- **CRM**: empresas, contatos e pipeline simples de negócios.
|
||||
|
||||
## Stack
|
||||
- Node.js + TypeScript
|
||||
- NestJS
|
||||
- PostgreSQL + Prisma
|
||||
- Docker
|
||||
|
||||
## Integração com identity-gateway
|
||||
- Todas as rotas são protegidas por JWT interno.
|
||||
- O token deve conter:
|
||||
- `tenantId`
|
||||
- `userId`
|
||||
- `roles`
|
||||
- O guard valida `issuer` e assina com `JWT_PUBLIC_KEY` ou `JWT_SECRET`.
|
||||
|
||||
## Modelo de dados (resumo)
|
||||
- **Tenant**: empresa cliente
|
||||
- **Plan**: preço, ciclo e limites
|
||||
- **Subscription**: tenant + plano
|
||||
- **Invoice**: contas a receber
|
||||
- **Payment**: pagamentos com gateway
|
||||
- **FiscalDocument**: base para NFS-e
|
||||
- **CRM**: companies, contacts, deals
|
||||
|
||||
## Fluxo de cobrança
|
||||
1. Criar plano
|
||||
2. Criar assinatura
|
||||
3. Gerar invoice
|
||||
4. Criar pagamento via gateway
|
||||
5. Receber webhook e conciliar
|
||||
|
||||
## Endpoints mínimos
|
||||
```
|
||||
POST /tenants
|
||||
GET /tenants
|
||||
POST /plans
|
||||
GET /plans
|
||||
POST /subscriptions
|
||||
GET /subscriptions
|
||||
POST /invoices
|
||||
GET /invoices
|
||||
POST /payments/:invoiceId
|
||||
POST /webhooks/:gateway
|
||||
GET /crm/companies
|
||||
POST /crm/deals
|
||||
GET /health
|
||||
```
|
||||
|
||||
## Configuração local
|
||||
```bash
|
||||
cp .env.example .env
|
||||
npm install
|
||||
npm run prisma:generate
|
||||
npm run prisma:migrate
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
## Docker
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
## Estrutura
|
||||
- `src/core`: guard e contexto do tenant
|
||||
- `src/modules`: domínios de negócio
|
||||
- `prisma/`: schema e migrations
|
||||
- `docs/`: documentação técnica
|
||||
30
billing-finance-core/docker-compose.yml
Normal file
30
billing-finance-core/docker-compose.yml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
version: '3.9'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_DB: billing_finance_core
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- '5432:5432'
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
billing-finance-core:
|
||||
build: .
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
PORT: 3000
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/billing_finance_core
|
||||
JWT_SECRET: change-me
|
||||
JWT_ISSUER: identity-gateway
|
||||
PAYMENT_WEBHOOK_SECRET: change-me
|
||||
ports:
|
||||
- '3000:3000'
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
18
billing-finance-core/docs/architecture.md
Normal file
18
billing-finance-core/docs/architecture.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Arquitetura do billing-finance-core
|
||||
|
||||
## Visão geral
|
||||
O serviço `billing-finance-core` é responsável pelo core financeiro, billing, fiscal e CRM da plataforma SaaS multi-tenant. Ele confia no `identity-gateway` para autenticação e recebe o `tenantId` via JWT interno.
|
||||
|
||||
## Principais componentes
|
||||
- **Core**: Guard de autenticação JWT e contexto de tenant.
|
||||
- **Módulos de domínio**: tenants, planos, assinaturas, invoices, payments, fiscal e CRM.
|
||||
- **Gateways de pagamento**: padrão Strategy para Pix, boleto e cartão.
|
||||
- **Persistência**: PostgreSQL com Prisma e migrations.
|
||||
|
||||
## Multi-tenant
|
||||
- Todas as rotas usam `tenantId` extraído do JWT interno.
|
||||
- Consultas sempre filtram por `tenantId`.
|
||||
|
||||
## Integrações
|
||||
- **Identity Gateway**: JWT interno contendo `tenantId`, `userId`, `roles`.
|
||||
- **Gateways de pagamento**: integração via webhooks e reconciliação idempotente.
|
||||
18
billing-finance-core/docs/billing-flow.md
Normal file
18
billing-finance-core/docs/billing-flow.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Fluxo de cobrança
|
||||
|
||||
1. **Criação de plano**
|
||||
- Define preço, ciclo de cobrança e limites.
|
||||
2. **Assinatura**
|
||||
- Relaciona tenant e plano, define datas de ciclo e status.
|
||||
3. **Invoice**
|
||||
- Conta a receber com vencimento e status.
|
||||
4. **Pagamento**
|
||||
- Gateway escolhido gera pagamento (Pix, boleto, cartão).
|
||||
5. **Webhook**
|
||||
- Gateway envia evento de pagamento.
|
||||
6. **Conciliação**
|
||||
- Atualiza status do pagamento e invoice.
|
||||
|
||||
## Status principais
|
||||
- **Invoice**: PENDING, PAID, OVERDUE, CANCELED
|
||||
- **Payment**: PENDING, CONFIRMED, FAILED, CANCELED
|
||||
12
billing-finance-core/docs/fiscal.md
Normal file
12
billing-finance-core/docs/fiscal.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Fiscal (base)
|
||||
|
||||
O módulo fiscal mantém informações básicas para emissão de NFS-e.
|
||||
|
||||
## Campos
|
||||
- Número da nota
|
||||
- Status (DRAFT, ISSUED, CANCELED)
|
||||
- Links de PDF/XML
|
||||
|
||||
## Integração futura
|
||||
- Preparado para integrar com provedor externo.
|
||||
- Não inclui regras municipais complexas.
|
||||
4
billing-finance-core/nest-cli.json
Normal file
4
billing-finance-core/nest-cli.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
||||
38
billing-finance-core/package.json
Normal file
38
billing-finance-core/package.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "billing-finance-core",
|
||||
"version": "1.0.0",
|
||||
"description": "Core financeiro, billing, fiscal e CRM para plataforma SaaS multi-tenant.",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:prod": "node dist/main.js",
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate deploy",
|
||||
"prisma:studio": "prisma studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.3.0",
|
||||
"@nestjs/core": "^10.3.0",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
"@prisma/client": "^5.20.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.2",
|
||||
"@nestjs/schematics": "^10.1.2",
|
||||
"@nestjs/testing": "^10.3.0",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^20.14.11",
|
||||
"prisma": "^5.20.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
166
billing-finance-core/prisma/migrations/000_init/migration.sql
Normal file
166
billing-finance-core/prisma/migrations/000_init/migration.sql
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "BillingCycle" AS ENUM ('MONTHLY', 'YEARLY');
|
||||
CREATE TYPE "SubscriptionStatus" AS ENUM ('ACTIVE', 'PAST_DUE', 'CANCELED', 'TRIAL');
|
||||
CREATE TYPE "InvoiceStatus" AS ENUM ('PENDING', 'PAID', 'OVERDUE', 'CANCELED');
|
||||
CREATE TYPE "PaymentStatus" AS ENUM ('PENDING', 'CONFIRMED', 'FAILED', 'CANCELED');
|
||||
CREATE TYPE "FiscalStatus" AS ENUM ('DRAFT', 'ISSUED', 'CANCELED');
|
||||
CREATE TYPE "DealStage" AS ENUM ('LEAD', 'PROPOSAL', 'NEGOTIATION', 'WON', 'LOST');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Tenant" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"taxId" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Tenant_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Plan" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"priceCents" INTEGER NOT NULL,
|
||||
"billingCycle" "BillingCycle" NOT NULL,
|
||||
"softLimit" INTEGER,
|
||||
"hardLimit" INTEGER,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Plan_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Subscription" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"planId" TEXT NOT NULL,
|
||||
"status" "SubscriptionStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"startDate" TIMESTAMP(3) NOT NULL,
|
||||
"nextDueDate" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Invoice" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"subscriptionId" TEXT,
|
||||
"amountCents" INTEGER NOT NULL,
|
||||
"dueDate" TIMESTAMP(3) NOT NULL,
|
||||
"status" "InvoiceStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"paidAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Invoice_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Payment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"invoiceId" TEXT NOT NULL,
|
||||
"gateway" TEXT NOT NULL,
|
||||
"method" TEXT NOT NULL,
|
||||
"externalId" TEXT,
|
||||
"status" "PaymentStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"amountCents" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Payment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "FiscalDocument" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"invoiceId" TEXT,
|
||||
"number" TEXT,
|
||||
"status" "FiscalStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"pdfUrl" TEXT,
|
||||
"xmlUrl" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "FiscalDocument_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CrmCompany" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"segment" TEXT,
|
||||
"website" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CrmCompany_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CrmContact" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"companyId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT,
|
||||
"phone" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CrmContact_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CrmDeal" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"companyId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"stage" "DealStage" NOT NULL DEFAULT 'LEAD',
|
||||
"valueCents" INTEGER NOT NULL,
|
||||
"expectedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "CrmDeal_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "Subscription_tenantId_idx" ON "Subscription"("tenantId");
|
||||
CREATE INDEX "Subscription_planId_idx" ON "Subscription"("planId");
|
||||
CREATE INDEX "Invoice_tenantId_idx" ON "Invoice"("tenantId");
|
||||
CREATE INDEX "Invoice_subscriptionId_idx" ON "Invoice"("subscriptionId");
|
||||
CREATE INDEX "Payment_tenantId_idx" ON "Payment"("tenantId");
|
||||
CREATE INDEX "Payment_invoiceId_idx" ON "Payment"("invoiceId");
|
||||
CREATE UNIQUE INDEX "Payment_gateway_externalId_key" ON "Payment"("gateway", "externalId");
|
||||
CREATE INDEX "FiscalDocument_tenantId_idx" ON "FiscalDocument"("tenantId");
|
||||
CREATE INDEX "FiscalDocument_invoiceId_idx" ON "FiscalDocument"("invoiceId");
|
||||
CREATE INDEX "CrmCompany_tenantId_idx" ON "CrmCompany"("tenantId");
|
||||
CREATE INDEX "CrmContact_tenantId_idx" ON "CrmContact"("tenantId");
|
||||
CREATE INDEX "CrmContact_companyId_idx" ON "CrmContact"("companyId");
|
||||
CREATE INDEX "CrmDeal_tenantId_idx" ON "CrmDeal"("tenantId");
|
||||
CREATE INDEX "CrmDeal_companyId_idx" ON "CrmDeal"("companyId");
|
||||
|
||||
-- Foreign Keys
|
||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_planId_fkey" FOREIGN KEY ("planId") REFERENCES "Plan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "Invoice" ADD CONSTRAINT "Invoice_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "Invoice" ADD CONSTRAINT "Invoice_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "Subscription"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "Invoice"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "FiscalDocument" ADD CONSTRAINT "FiscalDocument_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "FiscalDocument" ADD CONSTRAINT "FiscalDocument_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "Invoice"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "CrmCompany" ADD CONSTRAINT "CrmCompany_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "CrmContact" ADD CONSTRAINT "CrmContact_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "CrmContact" ADD CONSTRAINT "CrmContact_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "CrmCompany"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "CrmDeal" ADD CONSTRAINT "CrmDeal_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "CrmDeal" ADD CONSTRAINT "CrmDeal_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "CrmCompany"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
195
billing-finance-core/prisma/schema.prisma
Normal file
195
billing-finance-core/prisma/schema.prisma
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum BillingCycle {
|
||||
MONTHLY
|
||||
YEARLY
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
ACTIVE
|
||||
PAST_DUE
|
||||
CANCELED
|
||||
TRIAL
|
||||
}
|
||||
|
||||
enum InvoiceStatus {
|
||||
PENDING
|
||||
PAID
|
||||
OVERDUE
|
||||
CANCELED
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
PENDING
|
||||
CONFIRMED
|
||||
FAILED
|
||||
CANCELED
|
||||
}
|
||||
|
||||
enum FiscalStatus {
|
||||
DRAFT
|
||||
ISSUED
|
||||
CANCELED
|
||||
}
|
||||
|
||||
enum DealStage {
|
||||
LEAD
|
||||
PROPOSAL
|
||||
NEGOTIATION
|
||||
WON
|
||||
LOST
|
||||
}
|
||||
|
||||
model Tenant {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
taxId String?
|
||||
status String @default("ACTIVE")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
subscriptions Subscription[]
|
||||
invoices Invoice[]
|
||||
payments Payment[]
|
||||
fiscalDocs FiscalDocument[]
|
||||
crmCompanies CrmCompany[]
|
||||
crmContacts CrmContact[]
|
||||
crmDeals CrmDeal[]
|
||||
}
|
||||
|
||||
model Plan {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
priceCents Int
|
||||
billingCycle BillingCycle
|
||||
softLimit Int?
|
||||
hardLimit Int?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
subscriptions Subscription[]
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
planId String
|
||||
status SubscriptionStatus @default(ACTIVE)
|
||||
startDate DateTime
|
||||
nextDueDate DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
plan Plan @relation(fields: [planId], references: [id])
|
||||
invoices Invoice[]
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([planId])
|
||||
}
|
||||
|
||||
model Invoice {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
subscriptionId String?
|
||||
amountCents Int
|
||||
dueDate DateTime
|
||||
status InvoiceStatus @default(PENDING)
|
||||
paidAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
|
||||
payments Payment[]
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([subscriptionId])
|
||||
}
|
||||
|
||||
model Payment {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
invoiceId String
|
||||
gateway String
|
||||
method String
|
||||
externalId String?
|
||||
status PaymentStatus @default(PENDING)
|
||||
amountCents Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
invoice Invoice @relation(fields: [invoiceId], references: [id])
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([invoiceId])
|
||||
@@unique([gateway, externalId])
|
||||
}
|
||||
|
||||
model FiscalDocument {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
invoiceId String?
|
||||
number String?
|
||||
status FiscalStatus @default(DRAFT)
|
||||
pdfUrl String?
|
||||
xmlUrl String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
invoice Invoice? @relation(fields: [invoiceId], references: [id])
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([invoiceId])
|
||||
}
|
||||
|
||||
model CrmCompany {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
name String
|
||||
segment String?
|
||||
website String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
contacts CrmContact[]
|
||||
deals CrmDeal[]
|
||||
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
model CrmContact {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
companyId String?
|
||||
name String
|
||||
email String?
|
||||
phone String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
company CrmCompany? @relation(fields: [companyId], references: [id])
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([companyId])
|
||||
}
|
||||
|
||||
model CrmDeal {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
companyId String?
|
||||
name String
|
||||
stage DealStage @default(LEAD)
|
||||
valueCents Int
|
||||
expectedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
company CrmCompany? @relation(fields: [companyId], references: [id])
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([companyId])
|
||||
}
|
||||
9
billing-finance-core/src/app.controller.ts
Normal file
9
billing-finance-core/src/app.controller.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
@Get('health')
|
||||
getHealth() {
|
||||
return { status: 'ok' };
|
||||
}
|
||||
}
|
||||
35
billing-finance-core/src/app.module.ts
Normal file
35
billing-finance-core/src/app.module.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { AppController } from './app.controller';
|
||||
import { AuthGuard } from './core/auth.guard';
|
||||
import { PrismaService } from './lib/postgres';
|
||||
import { TenantModule } from './modules/tenants/tenant.module';
|
||||
import { PlanModule } from './modules/plans/plan.module';
|
||||
import { SubscriptionModule } from './modules/subscriptions/subscription.module';
|
||||
import { InvoiceModule } from './modules/invoices/invoice.module';
|
||||
import { PaymentModule } from './modules/payments/payment.module';
|
||||
import { WebhookModule } from './modules/webhooks/webhook.module';
|
||||
import { FiscalModule } from './modules/fiscal/fiscal.module';
|
||||
import { CrmModule } from './modules/crm/crm.module';
|
||||
|
||||
@Module({
|
||||
controllers: [AppController],
|
||||
imports: [
|
||||
TenantModule,
|
||||
PlanModule,
|
||||
SubscriptionModule,
|
||||
InvoiceModule,
|
||||
PaymentModule,
|
||||
WebhookModule,
|
||||
FiscalModule,
|
||||
CrmModule,
|
||||
],
|
||||
providers: [
|
||||
PrismaService,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: AuthGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
58
billing-finance-core/src/core/auth.guard.ts
Normal file
58
billing-finance-core/src/core/auth.guard.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||
import { env } from '../lib/env';
|
||||
|
||||
interface IdentityGatewayPayload extends JwtPayload {
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
if (request.path.startsWith('/webhooks')) {
|
||||
const secret = request.headers['x-webhook-secret'];
|
||||
if (secret && secret === env.webhookSecret) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
throw new UnauthorizedException('Missing bearer token');
|
||||
}
|
||||
|
||||
const token = authHeader.replace('Bearer ', '').trim();
|
||||
try {
|
||||
const verified = jwt.verify(
|
||||
token,
|
||||
env.jwtPublicKey || env.jwtSecret,
|
||||
{
|
||||
issuer: env.jwtIssuer,
|
||||
},
|
||||
) as IdentityGatewayPayload;
|
||||
|
||||
if (!verified.tenantId || !verified.userId) {
|
||||
throw new UnauthorizedException('Token missing tenant/user');
|
||||
}
|
||||
|
||||
request.user = {
|
||||
tenantId: verified.tenantId,
|
||||
userId: verified.userId,
|
||||
roles: verified.roles ?? [],
|
||||
} as IdentityGatewayPayload;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
}
|
||||
39
billing-finance-core/src/core/enums.ts
Normal file
39
billing-finance-core/src/core/enums.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
export enum BillingCycle {
|
||||
MONTHLY = 'MONTHLY',
|
||||
YEARLY = 'YEARLY',
|
||||
}
|
||||
|
||||
export enum SubscriptionStatus {
|
||||
ACTIVE = 'ACTIVE',
|
||||
PAST_DUE = 'PAST_DUE',
|
||||
CANCELED = 'CANCELED',
|
||||
TRIAL = 'TRIAL',
|
||||
}
|
||||
|
||||
export enum InvoiceStatus {
|
||||
PENDING = 'PENDING',
|
||||
PAID = 'PAID',
|
||||
OVERDUE = 'OVERDUE',
|
||||
CANCELED = 'CANCELED',
|
||||
}
|
||||
|
||||
export enum PaymentStatus {
|
||||
PENDING = 'PENDING',
|
||||
CONFIRMED = 'CONFIRMED',
|
||||
FAILED = 'FAILED',
|
||||
CANCELED = 'CANCELED',
|
||||
}
|
||||
|
||||
export enum FiscalStatus {
|
||||
DRAFT = 'DRAFT',
|
||||
ISSUED = 'ISSUED',
|
||||
CANCELED = 'CANCELED',
|
||||
}
|
||||
|
||||
export enum DealStage {
|
||||
LEAD = 'LEAD',
|
||||
PROPOSAL = 'PROPOSAL',
|
||||
NEGOTIATION = 'NEGOTIATION',
|
||||
WON = 'WON',
|
||||
LOST = 'LOST',
|
||||
}
|
||||
15
billing-finance-core/src/core/tenant.context.ts
Normal file
15
billing-finance-core/src/core/tenant.context.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Request } from 'express';
|
||||
|
||||
export interface TenantContextPayload {
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export const getTenantContext = (request: Request): TenantContextPayload => {
|
||||
const user = request.user as TenantContextPayload | undefined;
|
||||
if (!user) {
|
||||
throw new Error('Tenant context missing from request.');
|
||||
}
|
||||
return user;
|
||||
};
|
||||
2
billing-finance-core/src/database/prisma.schema
Normal file
2
billing-finance-core/src/database/prisma.schema
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Source of truth for database schema is /prisma/schema.prisma
|
||||
// This file documents the required structure inside src/database.
|
||||
15
billing-finance-core/src/lib/env.ts
Normal file
15
billing-finance-core/src/lib/env.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const env = {
|
||||
nodeEnv: process.env.NODE_ENV ?? 'development',
|
||||
port: Number(process.env.PORT ?? 3000),
|
||||
databaseUrl: process.env.DATABASE_URL ?? '',
|
||||
jwtSecret: process.env.JWT_SECRET ?? '',
|
||||
jwtPublicKey: process.env.JWT_PUBLIC_KEY ?? '',
|
||||
jwtIssuer: process.env.JWT_ISSUER ?? 'identity-gateway',
|
||||
webhookSecret: process.env.PAYMENT_WEBHOOK_SECRET ?? '',
|
||||
stripeApiKey: process.env.STRIPE_API_KEY ?? '',
|
||||
appLogLevel: process.env.APP_LOG_LEVEL ?? 'info',
|
||||
};
|
||||
3
billing-finance-core/src/lib/logger.ts
Normal file
3
billing-finance-core/src/lib/logger.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { Logger } from '@nestjs/common';
|
||||
|
||||
export const appLogger = new Logger('billing-finance-core');
|
||||
13
billing-finance-core/src/lib/postgres.ts
Normal file
13
billing-finance-core/src/lib/postgres.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
}
|
||||
}
|
||||
15
billing-finance-core/src/main.ts
Normal file
15
billing-finance-core/src/main.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import 'reflect-metadata';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
import { env } from './lib/env';
|
||||
import { appLogger } from './lib/logger';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, { logger: appLogger });
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
||||
await app.listen(env.port);
|
||||
appLogger.log(`billing-finance-core running on port ${env.port}`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
9
billing-finance-core/src/modules/crm/company.entity.ts
Normal file
9
billing-finance-core/src/modules/crm/company.entity.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export class CompanyEntity {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
name: string;
|
||||
segment?: string;
|
||||
website?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
10
billing-finance-core/src/modules/crm/contact.entity.ts
Normal file
10
billing-finance-core/src/modules/crm/contact.entity.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export class ContactEntity {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId?: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
93
billing-finance-core/src/modules/crm/crm.controller.ts
Normal file
93
billing-finance-core/src/modules/crm/crm.controller.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { Body, Controller, Get, Post, Req } from '@nestjs/common';
|
||||
import { IsDateString, IsEnum, IsInt, IsOptional, IsString, Min } from 'class-validator';
|
||||
import { Request } from 'express';
|
||||
import { DealStage } from '../../core/enums';
|
||||
import { getTenantContext } from '../../core/tenant.context';
|
||||
import { CrmService } from './crm.service';
|
||||
|
||||
class CreateCompanyDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
segment?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
website?: string;
|
||||
}
|
||||
|
||||
class CreateContactDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
companyId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
class CreateDealDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
companyId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(DealStage)
|
||||
stage?: DealStage;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
valueCents: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
expectedAt?: string;
|
||||
}
|
||||
|
||||
@Controller('crm')
|
||||
export class CrmController {
|
||||
constructor(private readonly crmService: CrmService) {}
|
||||
|
||||
@Get('companies')
|
||||
listCompanies(@Req() req: Request) {
|
||||
const { tenantId } = getTenantContext(req);
|
||||
return this.crmService.listCompanies(tenantId);
|
||||
}
|
||||
|
||||
@Post('companies')
|
||||
createCompany(@Req() req: Request, @Body() body: CreateCompanyDto) {
|
||||
const { tenantId } = getTenantContext(req);
|
||||
return this.crmService.createCompany({ tenantId, ...body });
|
||||
}
|
||||
|
||||
@Post('contacts')
|
||||
createContact(@Req() req: Request, @Body() body: CreateContactDto) {
|
||||
const { tenantId } = getTenantContext(req);
|
||||
return this.crmService.createContact({ tenantId, ...body });
|
||||
}
|
||||
|
||||
@Post('deals')
|
||||
createDeal(@Req() req: Request, @Body() body: CreateDealDto) {
|
||||
const { tenantId } = getTenantContext(req);
|
||||
return this.crmService.createDeal({
|
||||
tenantId,
|
||||
companyId: body.companyId,
|
||||
name: body.name,
|
||||
stage: body.stage,
|
||||
valueCents: body.valueCents,
|
||||
expectedAt: body.expectedAt ? new Date(body.expectedAt) : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
11
billing-finance-core/src/modules/crm/crm.module.ts
Normal file
11
billing-finance-core/src/modules/crm/crm.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { CrmController } from './crm.controller';
|
||||
import { CrmService } from './crm.service';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
@Module({
|
||||
controllers: [CrmController],
|
||||
providers: [CrmService, PrismaService],
|
||||
exports: [CrmService],
|
||||
})
|
||||
export class CrmModule {}
|
||||
60
billing-finance-core/src/modules/crm/crm.service.ts
Normal file
60
billing-finance-core/src/modules/crm/crm.service.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { DealStage } from '../../core/enums';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
interface CreateCompanyInput {
|
||||
tenantId: string;
|
||||
name: string;
|
||||
segment?: string;
|
||||
website?: string;
|
||||
}
|
||||
|
||||
interface CreateContactInput {
|
||||
tenantId: string;
|
||||
companyId?: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
interface CreateDealInput {
|
||||
tenantId: string;
|
||||
companyId?: string;
|
||||
name: string;
|
||||
stage?: DealStage;
|
||||
valueCents: number;
|
||||
expectedAt?: Date;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CrmService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
createCompany(data: CreateCompanyInput) {
|
||||
return this.prisma.crmCompany.create({ data });
|
||||
}
|
||||
|
||||
listCompanies(tenantId: string) {
|
||||
return this.prisma.crmCompany.findMany({
|
||||
where: { tenantId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
createContact(data: CreateContactInput) {
|
||||
return this.prisma.crmContact.create({ data });
|
||||
}
|
||||
|
||||
createDeal(data: CreateDealInput) {
|
||||
return this.prisma.crmDeal.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
companyId: data.companyId,
|
||||
name: data.name,
|
||||
stage: data.stage ?? DealStage.LEAD,
|
||||
valueCents: data.valueCents,
|
||||
expectedAt: data.expectedAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
13
billing-finance-core/src/modules/crm/deal.entity.ts
Normal file
13
billing-finance-core/src/modules/crm/deal.entity.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { DealStage } from '../../core/enums';
|
||||
|
||||
export class DealEntity {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
companyId?: string;
|
||||
name: string;
|
||||
stage: DealStage;
|
||||
valueCents: number;
|
||||
expectedAt?: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
13
billing-finance-core/src/modules/fiscal/fiscal.entity.ts
Normal file
13
billing-finance-core/src/modules/fiscal/fiscal.entity.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { FiscalStatus } from '../../core/enums';
|
||||
|
||||
export class FiscalEntity {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
invoiceId?: string;
|
||||
number?: string;
|
||||
status: FiscalStatus;
|
||||
pdfUrl?: string;
|
||||
xmlUrl?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
9
billing-finance-core/src/modules/fiscal/fiscal.module.ts
Normal file
9
billing-finance-core/src/modules/fiscal/fiscal.module.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { FiscalService } from './fiscal.service';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
@Module({
|
||||
providers: [FiscalService, PrismaService],
|
||||
exports: [FiscalService],
|
||||
})
|
||||
export class FiscalModule {}
|
||||
37
billing-finance-core/src/modules/fiscal/fiscal.service.ts
Normal file
37
billing-finance-core/src/modules/fiscal/fiscal.service.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { FiscalStatus } from '../../core/enums';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
interface CreateFiscalInput {
|
||||
tenantId: string;
|
||||
invoiceId?: string;
|
||||
number?: string;
|
||||
status?: FiscalStatus;
|
||||
pdfUrl?: string;
|
||||
xmlUrl?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class FiscalService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
create(data: CreateFiscalInput) {
|
||||
return this.prisma.fiscalDocument.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
invoiceId: data.invoiceId,
|
||||
number: data.number,
|
||||
status: data.status ?? FiscalStatus.DRAFT,
|
||||
pdfUrl: data.pdfUrl,
|
||||
xmlUrl: data.xmlUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
list(tenantId: string) {
|
||||
return this.prisma.fiscalDocument.findMany({
|
||||
where: { tenantId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { Body, Controller, Get, Post, Req } from '@nestjs/common';
|
||||
import { IsDateString, IsEnum, IsInt, IsOptional, IsString, Min } from 'class-validator';
|
||||
import { Request } from 'express';
|
||||
import { InvoiceStatus } from '../../core/enums';
|
||||
import { getTenantContext } from '../../core/tenant.context';
|
||||
import { InvoiceService } from './invoice.service';
|
||||
|
||||
class CreateInvoiceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
subscriptionId?: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
amountCents: number;
|
||||
|
||||
@IsDateString()
|
||||
dueDate: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(InvoiceStatus)
|
||||
status?: InvoiceStatus;
|
||||
}
|
||||
|
||||
@Controller('invoices')
|
||||
export class InvoiceController {
|
||||
constructor(private readonly invoiceService: InvoiceService) {}
|
||||
|
||||
@Post()
|
||||
create(@Req() req: Request, @Body() body: CreateInvoiceDto) {
|
||||
const { tenantId } = getTenantContext(req);
|
||||
return this.invoiceService.create({
|
||||
tenantId,
|
||||
subscriptionId: body.subscriptionId,
|
||||
amountCents: body.amountCents,
|
||||
dueDate: new Date(body.dueDate),
|
||||
status: body.status,
|
||||
});
|
||||
}
|
||||
|
||||
@Get()
|
||||
list(@Req() req: Request) {
|
||||
const { tenantId } = getTenantContext(req);
|
||||
return this.invoiceService.list(tenantId);
|
||||
}
|
||||
}
|
||||
13
billing-finance-core/src/modules/invoices/invoice.entity.ts
Normal file
13
billing-finance-core/src/modules/invoices/invoice.entity.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { InvoiceStatus } from '../../core/enums';
|
||||
|
||||
export class InvoiceEntity {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
subscriptionId?: string;
|
||||
amountCents: number;
|
||||
dueDate: Date;
|
||||
status: InvoiceStatus;
|
||||
paidAt?: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
11
billing-finance-core/src/modules/invoices/invoice.module.ts
Normal file
11
billing-finance-core/src/modules/invoices/invoice.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { InvoiceController } from './invoice.controller';
|
||||
import { InvoiceService } from './invoice.service';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
@Module({
|
||||
controllers: [InvoiceController],
|
||||
providers: [InvoiceService, PrismaService],
|
||||
exports: [InvoiceService],
|
||||
})
|
||||
export class InvoiceModule {}
|
||||
36
billing-finance-core/src/modules/invoices/invoice.service.ts
Normal file
36
billing-finance-core/src/modules/invoices/invoice.service.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InvoiceStatus } from '../../core/enums';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
interface CreateInvoiceInput {
|
||||
tenantId: string;
|
||||
subscriptionId?: string;
|
||||
amountCents: number;
|
||||
dueDate: Date;
|
||||
status?: InvoiceStatus;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class InvoiceService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
create(data: CreateInvoiceInput) {
|
||||
return this.prisma.invoice.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
subscriptionId: data.subscriptionId,
|
||||
amountCents: data.amountCents,
|
||||
dueDate: data.dueDate,
|
||||
status: data.status ?? InvoiceStatus.PENDING,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
list(tenantId: string) {
|
||||
return this.prisma.invoice.findMany({
|
||||
where: { tenantId },
|
||||
include: { payments: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
import { Invoice, Payment } from '@prisma/client';
|
||||
import { PaymentStatus } from '../../../core/enums';
|
||||
import { PaymentGateway, PaymentGatewayResult, PaymentWebhookResult } from './gateway.interface';
|
||||
|
||||
export class BoletoGateway implements PaymentGateway {
|
||||
name = 'boleto';
|
||||
|
||||
async createPayment(invoice: Invoice): Promise<PaymentGatewayResult> {
|
||||
const externalId = `boleto_${randomUUID()}`;
|
||||
return {
|
||||
externalId,
|
||||
status: PaymentStatus.PENDING,
|
||||
metadata: {
|
||||
barcode: `34191.79001 ${externalId.slice(0, 8)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async handleWebhook(payload: any): Promise<PaymentWebhookResult> {
|
||||
return {
|
||||
externalId: payload?.externalId ?? '',
|
||||
status: payload?.status ?? PaymentStatus.CONFIRMED,
|
||||
};
|
||||
}
|
||||
|
||||
async reconcile(payment: Payment): Promise<PaymentStatus> {
|
||||
return payment.status;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
import { Invoice, Payment } from '@prisma/client';
|
||||
import { PaymentStatus } from '../../../core/enums';
|
||||
import { env } from '../../../lib/env';
|
||||
import { appLogger } from '../../../lib/logger';
|
||||
import { PaymentGateway, PaymentGatewayResult, PaymentWebhookResult } from './gateway.interface';
|
||||
|
||||
export class CardGateway implements PaymentGateway {
|
||||
name = 'card';
|
||||
|
||||
async createPayment(invoice: Invoice): Promise<PaymentGatewayResult> {
|
||||
if (!env.stripeApiKey) {
|
||||
const externalId = `card_${randomUUID()}`;
|
||||
return {
|
||||
externalId,
|
||||
status: PaymentStatus.PENDING,
|
||||
metadata: { provider: 'mock' },
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.stripe.com/v1/payment_intents', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.stripeApiKey}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
amount: invoice.amountCents.toString(),
|
||||
currency: 'brl',
|
||||
'metadata[invoiceId]': invoice.id,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
appLogger.error(`Stripe error: ${errorText}`);
|
||||
return {
|
||||
status: PaymentStatus.FAILED,
|
||||
};
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as { id: string };
|
||||
return {
|
||||
externalId: payload.id,
|
||||
status: PaymentStatus.PENDING,
|
||||
};
|
||||
}
|
||||
|
||||
async handleWebhook(payload: any): Promise<PaymentWebhookResult> {
|
||||
return {
|
||||
externalId: payload?.data?.object?.id ?? payload?.externalId ?? '',
|
||||
status: payload?.status ?? PaymentStatus.CONFIRMED,
|
||||
};
|
||||
}
|
||||
|
||||
async reconcile(payment: Payment): Promise<PaymentStatus> {
|
||||
return payment.status;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Invoice, Payment } from '@prisma/client';
|
||||
import { PaymentStatus } from '../../../core/enums';
|
||||
|
||||
export interface PaymentGatewayResult {
|
||||
externalId?: string;
|
||||
status: PaymentStatus;
|
||||
redirectUrl?: string;
|
||||
qrCode?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PaymentWebhookResult {
|
||||
externalId: string;
|
||||
status: PaymentStatus;
|
||||
}
|
||||
|
||||
export interface PaymentGateway {
|
||||
name: string;
|
||||
createPayment(invoice: Invoice): Promise<PaymentGatewayResult>;
|
||||
handleWebhook(payload: unknown, signature?: string): Promise<PaymentWebhookResult>;
|
||||
reconcile(payment: Payment): Promise<PaymentStatus>;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
import { Invoice, Payment } from '@prisma/client';
|
||||
import { PaymentStatus } from '../../../core/enums';
|
||||
import { PaymentGateway, PaymentGatewayResult, PaymentWebhookResult } from './gateway.interface';
|
||||
|
||||
export class PixGateway implements PaymentGateway {
|
||||
name = 'pix';
|
||||
|
||||
async createPayment(invoice: Invoice): Promise<PaymentGatewayResult> {
|
||||
const externalId = `pix_${randomUUID()}`;
|
||||
return {
|
||||
externalId,
|
||||
status: PaymentStatus.PENDING,
|
||||
qrCode: `PIX-QRCODE-${externalId}`,
|
||||
};
|
||||
}
|
||||
|
||||
async handleWebhook(payload: any): Promise<PaymentWebhookResult> {
|
||||
return {
|
||||
externalId: payload?.externalId ?? '',
|
||||
status: payload?.status ?? PaymentStatus.CONFIRMED,
|
||||
};
|
||||
}
|
||||
|
||||
async reconcile(payment: Payment): Promise<PaymentStatus> {
|
||||
return payment.status;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { Body, Controller, Param, Post, Req } from '@nestjs/common';
|
||||
import { IsEnum, IsString } from 'class-validator';
|
||||
import { Request } from 'express';
|
||||
import { getTenantContext } from '../../core/tenant.context';
|
||||
import { PaymentService } from './payment.service';
|
||||
|
||||
enum PaymentMethod {
|
||||
PIX = 'pix',
|
||||
BOLETO = 'boleto',
|
||||
CARD = 'card',
|
||||
}
|
||||
|
||||
class CreatePaymentDto {
|
||||
@IsEnum(PaymentMethod)
|
||||
method: PaymentMethod;
|
||||
|
||||
@IsString()
|
||||
gateway: string;
|
||||
}
|
||||
|
||||
@Controller('payments')
|
||||
export class PaymentController {
|
||||
constructor(private readonly paymentService: PaymentService) {}
|
||||
|
||||
@Post(':invoiceId')
|
||||
create(
|
||||
@Req() req: Request,
|
||||
@Param('invoiceId') invoiceId: string,
|
||||
@Body() body: CreatePaymentDto,
|
||||
) {
|
||||
const { tenantId } = getTenantContext(req);
|
||||
return this.paymentService.createPayment({
|
||||
tenantId,
|
||||
invoiceId,
|
||||
method: body.method,
|
||||
gateway: body.gateway,
|
||||
});
|
||||
}
|
||||
}
|
||||
14
billing-finance-core/src/modules/payments/payment.entity.ts
Normal file
14
billing-finance-core/src/modules/payments/payment.entity.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { PaymentStatus } from '../../core/enums';
|
||||
|
||||
export class PaymentEntity {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
invoiceId: string;
|
||||
gateway: string;
|
||||
method: string;
|
||||
externalId?: string;
|
||||
status: PaymentStatus;
|
||||
amountCents: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
11
billing-finance-core/src/modules/payments/payment.module.ts
Normal file
11
billing-finance-core/src/modules/payments/payment.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PaymentController } from './payment.controller';
|
||||
import { PaymentService } from './payment.service';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
@Module({
|
||||
controllers: [PaymentController],
|
||||
providers: [PaymentService, PrismaService],
|
||||
exports: [PaymentService],
|
||||
})
|
||||
export class PaymentModule {}
|
||||
82
billing-finance-core/src/modules/payments/payment.service.ts
Normal file
82
billing-finance-core/src/modules/payments/payment.service.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
import { PaymentStatus } from '../../core/enums';
|
||||
import { PaymentGateway } from './gateways/gateway.interface';
|
||||
import { PixGateway } from './gateways/pix.gateway';
|
||||
import { BoletoGateway } from './gateways/boleto.gateway';
|
||||
import { CardGateway } from './gateways/card.gateway';
|
||||
|
||||
interface CreatePaymentInput {
|
||||
tenantId: string;
|
||||
invoiceId: string;
|
||||
method: string;
|
||||
gateway: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PaymentService {
|
||||
private readonly gateways: Record<string, PaymentGateway>;
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {
|
||||
const available = [new PixGateway(), new BoletoGateway(), new CardGateway()];
|
||||
this.gateways = available.reduce((acc, gateway) => {
|
||||
acc[gateway.name] = gateway;
|
||||
return acc;
|
||||
}, {} as Record<string, PaymentGateway>);
|
||||
}
|
||||
|
||||
private getGateway(name: string): PaymentGateway {
|
||||
const gateway = this.gateways[name];
|
||||
if (!gateway) {
|
||||
throw new NotFoundException(`Gateway ${name} not supported`);
|
||||
}
|
||||
return gateway;
|
||||
}
|
||||
|
||||
async createPayment(data: CreatePaymentInput) {
|
||||
const invoice = await this.prisma.invoice.findFirst({
|
||||
where: { id: data.invoiceId, tenantId: data.tenantId },
|
||||
});
|
||||
|
||||
if (!invoice) {
|
||||
throw new NotFoundException('Invoice not found');
|
||||
}
|
||||
|
||||
const gateway = this.getGateway(data.gateway);
|
||||
const gatewayResult = await gateway.createPayment(invoice);
|
||||
|
||||
return this.prisma.payment.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
invoiceId: invoice.id,
|
||||
gateway: gateway.name,
|
||||
method: data.method,
|
||||
externalId: gatewayResult.externalId,
|
||||
status: gatewayResult.status ?? PaymentStatus.PENDING,
|
||||
amountCents: invoice.amountCents,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async reconcileByWebhook(gatewayName: string, payload: unknown) {
|
||||
const gateway = this.getGateway(gatewayName);
|
||||
const webhook = await gateway.handleWebhook(payload);
|
||||
|
||||
const existing = await this.prisma.payment.findFirst({
|
||||
where: { gateway: gatewayName, externalId: webhook.externalId },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return { ignored: true, reason: 'payment-not-found' };
|
||||
}
|
||||
|
||||
if (existing.status === webhook.status) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return this.prisma.payment.update({
|
||||
where: { id: existing.id },
|
||||
data: { status: webhook.status },
|
||||
});
|
||||
}
|
||||
}
|
||||
39
billing-finance-core/src/modules/plans/plan.controller.ts
Normal file
39
billing-finance-core/src/modules/plans/plan.controller.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||
import { IsEnum, IsInt, IsOptional, IsString, Min } from 'class-validator';
|
||||
import { BillingCycle } from '../../core/enums';
|
||||
import { PlanService } from './plan.service';
|
||||
|
||||
class CreatePlanDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
priceCents: number;
|
||||
|
||||
@IsEnum(BillingCycle)
|
||||
billingCycle: BillingCycle;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
softLimit?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
hardLimit?: number;
|
||||
}
|
||||
|
||||
@Controller('plans')
|
||||
export class PlanController {
|
||||
constructor(private readonly planService: PlanService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() body: CreatePlanDto) {
|
||||
return this.planService.create(body);
|
||||
}
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
return this.planService.list();
|
||||
}
|
||||
}
|
||||
12
billing-finance-core/src/modules/plans/plan.entity.ts
Normal file
12
billing-finance-core/src/modules/plans/plan.entity.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { BillingCycle } from '../../core/enums';
|
||||
|
||||
export class PlanEntity {
|
||||
id: string;
|
||||
name: string;
|
||||
priceCents: number;
|
||||
billingCycle: BillingCycle;
|
||||
softLimit?: number;
|
||||
hardLimit?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
11
billing-finance-core/src/modules/plans/plan.module.ts
Normal file
11
billing-finance-core/src/modules/plans/plan.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PlanController } from './plan.controller';
|
||||
import { PlanService } from './plan.service';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
@Module({
|
||||
controllers: [PlanController],
|
||||
providers: [PlanService, PrismaService],
|
||||
exports: [PlanService],
|
||||
})
|
||||
export class PlanModule {}
|
||||
24
billing-finance-core/src/modules/plans/plan.service.ts
Normal file
24
billing-finance-core/src/modules/plans/plan.service.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { BillingCycle } from '../../core/enums';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
interface CreatePlanInput {
|
||||
name: string;
|
||||
priceCents: number;
|
||||
billingCycle: BillingCycle;
|
||||
softLimit?: number;
|
||||
hardLimit?: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PlanService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
create(data: CreatePlanInput) {
|
||||
return this.prisma.plan.create({ data });
|
||||
}
|
||||
|
||||
list() {
|
||||
return this.prisma.plan.findMany({ orderBy: { createdAt: 'desc' } });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { Body, Controller, Get, Post, Req } from '@nestjs/common';
|
||||
import { IsDateString, IsEnum, IsOptional, IsString } from 'class-validator';
|
||||
import { Request } from 'express';
|
||||
import { SubscriptionStatus } from '../../core/enums';
|
||||
import { getTenantContext } from '../../core/tenant.context';
|
||||
import { SubscriptionService } from './subscription.service';
|
||||
|
||||
class CreateSubscriptionDto {
|
||||
@IsString()
|
||||
planId: string;
|
||||
|
||||
@IsDateString()
|
||||
startDate: string;
|
||||
|
||||
@IsDateString()
|
||||
nextDueDate: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(SubscriptionStatus)
|
||||
status?: SubscriptionStatus;
|
||||
}
|
||||
|
||||
@Controller('subscriptions')
|
||||
export class SubscriptionController {
|
||||
constructor(private readonly subscriptionService: SubscriptionService) {}
|
||||
|
||||
@Post()
|
||||
create(@Req() req: Request, @Body() body: CreateSubscriptionDto) {
|
||||
const { tenantId } = getTenantContext(req);
|
||||
return this.subscriptionService.create({
|
||||
tenantId,
|
||||
planId: body.planId,
|
||||
startDate: new Date(body.startDate),
|
||||
nextDueDate: new Date(body.nextDueDate),
|
||||
status: body.status,
|
||||
});
|
||||
}
|
||||
|
||||
@Get()
|
||||
list(@Req() req: Request) {
|
||||
const { tenantId } = getTenantContext(req);
|
||||
return this.subscriptionService.list(tenantId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { SubscriptionStatus } from '../../core/enums';
|
||||
|
||||
export class SubscriptionEntity {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
planId: string;
|
||||
status: SubscriptionStatus;
|
||||
startDate: Date;
|
||||
nextDueDate: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SubscriptionController } from './subscription.controller';
|
||||
import { SubscriptionService } from './subscription.service';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
@Module({
|
||||
controllers: [SubscriptionController],
|
||||
providers: [SubscriptionService, PrismaService],
|
||||
exports: [SubscriptionService],
|
||||
})
|
||||
export class SubscriptionModule {}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { SubscriptionStatus } from '../../core/enums';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
interface CreateSubscriptionInput {
|
||||
tenantId: string;
|
||||
planId: string;
|
||||
startDate: Date;
|
||||
nextDueDate: Date;
|
||||
status?: SubscriptionStatus;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
create(data: CreateSubscriptionInput) {
|
||||
return this.prisma.subscription.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
planId: data.planId,
|
||||
startDate: data.startDate,
|
||||
nextDueDate: data.nextDueDate,
|
||||
status: data.status ?? SubscriptionStatus.ACTIVE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
list(tenantId: string) {
|
||||
return this.prisma.subscription.findMany({
|
||||
where: { tenantId },
|
||||
include: { plan: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { TenantService } from './tenant.service';
|
||||
|
||||
class CreateTenantDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
taxId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
}
|
||||
|
||||
@Controller('tenants')
|
||||
export class TenantController {
|
||||
constructor(private readonly tenantService: TenantService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() body: CreateTenantDto) {
|
||||
return this.tenantService.create(body);
|
||||
}
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
return this.tenantService.list();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export class TenantEntity {
|
||||
id: string;
|
||||
name: string;
|
||||
taxId?: string;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
11
billing-finance-core/src/modules/tenants/tenant.module.ts
Normal file
11
billing-finance-core/src/modules/tenants/tenant.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TenantController } from './tenant.controller';
|
||||
import { TenantService } from './tenant.service';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
@Module({
|
||||
controllers: [TenantController],
|
||||
providers: [TenantService, PrismaService],
|
||||
exports: [TenantService],
|
||||
})
|
||||
export class TenantModule {}
|
||||
29
billing-finance-core/src/modules/tenants/tenant.service.ts
Normal file
29
billing-finance-core/src/modules/tenants/tenant.service.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
interface CreateTenantInput {
|
||||
name: string;
|
||||
taxId?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TenantService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
create(data: CreateTenantInput) {
|
||||
return this.prisma.tenant.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
taxId: data.taxId,
|
||||
status: data.status ?? 'ACTIVE',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
list() {
|
||||
return this.prisma.tenant.findMany({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Body, Controller, Param, Post } from '@nestjs/common';
|
||||
import { WebhookService } from './webhook.service';
|
||||
|
||||
@Controller('webhooks')
|
||||
export class WebhookController {
|
||||
constructor(private readonly webhookService: WebhookService) {}
|
||||
|
||||
@Post(':gateway')
|
||||
handle(@Param('gateway') gateway: string, @Body() payload: unknown) {
|
||||
return this.webhookService.handleGatewayWebhook(gateway, payload);
|
||||
}
|
||||
}
|
||||
11
billing-finance-core/src/modules/webhooks/webhook.module.ts
Normal file
11
billing-finance-core/src/modules/webhooks/webhook.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { WebhookController } from './webhook.controller';
|
||||
import { WebhookService } from './webhook.service';
|
||||
import { PaymentService } from '../payments/payment.service';
|
||||
import { PrismaService } from '../../lib/postgres';
|
||||
|
||||
@Module({
|
||||
controllers: [WebhookController],
|
||||
providers: [WebhookService, PaymentService, PrismaService],
|
||||
})
|
||||
export class WebhookModule {}
|
||||
11
billing-finance-core/src/modules/webhooks/webhook.service.ts
Normal file
11
billing-finance-core/src/modules/webhooks/webhook.service.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { PaymentService } from '../payments/payment.service';
|
||||
|
||||
@Injectable()
|
||||
export class WebhookService {
|
||||
constructor(private readonly paymentService: PaymentService) {}
|
||||
|
||||
handleGatewayWebhook(gateway: string, payload: unknown) {
|
||||
return this.paymentService.reconcileByWebhook(gateway, payload);
|
||||
}
|
||||
}
|
||||
17
billing-finance-core/tsconfig.json
Normal file
17
billing-finance-core/tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Loading…
Reference in a new issue