update subscription algorithm
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE "pricing_configs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"plan" TEXT NOT NULL,
|
||||
"billingPeriod" TEXT NOT NULL,
|
||||
"amount" INTEGER NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"updatedBy" TEXT,
|
||||
|
||||
CONSTRAINT "pricing_configs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "pricing_configs_plan_billingPeriod_key" ON "pricing_configs"("plan", "billingPeriod");
|
||||
|
||||
-- Seed default prices (amounts in centimes MAD)
|
||||
INSERT INTO "pricing_configs" ("id", "plan", "billingPeriod", "amount", "updatedAt") VALUES
|
||||
('prc_starter_monthly', 'STARTER', 'MONTHLY', 2990, NOW()),
|
||||
('prc_starter_annual', 'STARTER', 'ANNUAL', 29900, NOW()),
|
||||
('prc_growth_monthly', 'GROWTH', 'MONTHLY', 3990, NOW()),
|
||||
('prc_growth_annual', 'GROWTH', 'ANNUAL', 39900, NOW()),
|
||||
('prc_pro_monthly', 'PRO', 'MONTHLY', 99900, NOW()),
|
||||
('prc_pro_annual', 'PRO', 'ANNUAL', 999000, NOW());
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
-- Add new SubscriptionStatus enum values
|
||||
ALTER TYPE "SubscriptionStatus" ADD VALUE IF NOT EXISTS 'PAYMENT_PENDING';
|
||||
ALTER TYPE "SubscriptionStatus" ADD VALUE IF NOT EXISTS 'SUSPENDED';
|
||||
ALTER TYPE "SubscriptionStatus" ADD VALUE IF NOT EXISTS 'EXPIRED';
|
||||
ALTER TYPE "SubscriptionStatus" ADD VALUE IF NOT EXISTS 'PAUSED';
|
||||
|
||||
-- Add new InvoiceStatus enum value
|
||||
ALTER TYPE "InvoiceStatus" ADD VALUE IF NOT EXISTS 'VOIDED';
|
||||
|
||||
-- Extend subscriptions table (camelCase to match Prisma default)
|
||||
ALTER TABLE "subscriptions"
|
||||
ADD COLUMN IF NOT EXISTS "trialUsed" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS "paymentPendingSince" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "paymentDueAt" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "pastDueSince" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "suspendedAt" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "endedAt" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "retryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS "maxRetryCount" INTEGER NOT NULL DEFAULT 5;
|
||||
|
||||
-- Extend subscription_invoices table
|
||||
ALTER TABLE "subscription_invoices"
|
||||
ADD COLUMN IF NOT EXISTS "providerInvoiceId" TEXT,
|
||||
ADD COLUMN IF NOT EXISTS "dueAt" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "failedAt" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "voidedAt" TIMESTAMP(3);
|
||||
|
||||
-- Create subscription_events table
|
||||
CREATE TABLE IF NOT EXISTS "subscription_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"subscriptionId" TEXT NOT NULL,
|
||||
"companyId" TEXT NOT NULL,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"source" TEXT NOT NULL DEFAULT 'system',
|
||||
"payload" JSONB NOT NULL DEFAULT '{}',
|
||||
"occurredAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "subscription_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "subscription_events_subscriptionId_idx" ON "subscription_events"("subscriptionId");
|
||||
CREATE INDEX IF NOT EXISTS "subscription_events_companyId_idx" ON "subscription_events"("companyId");
|
||||
|
||||
ALTER TABLE "subscription_events"
|
||||
ADD CONSTRAINT "subscription_events_subscriptionId_fkey"
|
||||
FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Create payment_attempts table
|
||||
CREATE TABLE IF NOT EXISTS "payment_attempts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"invoiceId" TEXT NOT NULL,
|
||||
"subscriptionId" TEXT NOT NULL,
|
||||
"providerPaymentId" TEXT,
|
||||
"status" TEXT NOT NULL,
|
||||
"failureCode" TEXT,
|
||||
"failureMessage" TEXT,
|
||||
"attemptedAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "payment_attempts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "payment_attempts_invoiceId_idx" ON "payment_attempts"("invoiceId");
|
||||
|
||||
ALTER TABLE "payment_attempts"
|
||||
ADD CONSTRAINT "payment_attempts_invoiceId_fkey"
|
||||
FOREIGN KEY ("invoiceId") REFERENCES "subscription_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -35,8 +35,12 @@ enum BillingPeriod {
|
||||
enum SubscriptionStatus {
|
||||
TRIALING
|
||||
ACTIVE
|
||||
PAYMENT_PENDING
|
||||
PAST_DUE
|
||||
SUSPENDED
|
||||
CANCELLED
|
||||
EXPIRED
|
||||
PAUSED
|
||||
UNPAID
|
||||
}
|
||||
|
||||
@@ -45,6 +49,7 @@ enum InvoiceStatus {
|
||||
PAID
|
||||
FAILED
|
||||
REFUNDED
|
||||
VOIDED
|
||||
}
|
||||
|
||||
enum PaymentProvider {
|
||||
@@ -357,20 +362,29 @@ model Company {
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(cuid())
|
||||
companyId String @unique
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
plan Plan @default(STARTER)
|
||||
billingPeriod BillingPeriod @default(MONTHLY)
|
||||
status SubscriptionStatus @default(TRIALING)
|
||||
currency String @default("MAD")
|
||||
trialStartAt DateTime?
|
||||
trialEndAt DateTime?
|
||||
currentPeriodStart DateTime?
|
||||
currentPeriodEnd DateTime?
|
||||
cancelledAt DateTime?
|
||||
cancelAtPeriodEnd Boolean @default(false)
|
||||
invoices SubscriptionInvoice[]
|
||||
id String @id @default(cuid())
|
||||
companyId String @unique
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
plan Plan @default(STARTER)
|
||||
billingPeriod BillingPeriod @default(MONTHLY)
|
||||
status SubscriptionStatus @default(TRIALING)
|
||||
currency String @default("MAD")
|
||||
trialStartAt DateTime?
|
||||
trialEndAt DateTime?
|
||||
trialUsed Boolean @default(false)
|
||||
currentPeriodStart DateTime?
|
||||
currentPeriodEnd DateTime?
|
||||
paymentPendingSince DateTime?
|
||||
paymentDueAt DateTime?
|
||||
pastDueSince DateTime?
|
||||
suspendedAt DateTime?
|
||||
endedAt DateTime?
|
||||
cancelledAt DateTime?
|
||||
cancelAtPeriodEnd Boolean @default(false)
|
||||
retryCount Int @default(0)
|
||||
maxRetryCount Int @default(5)
|
||||
invoices SubscriptionInvoice[]
|
||||
events SubscriptionEvent[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -378,19 +392,40 @@ model Subscription {
|
||||
@@map("subscriptions")
|
||||
}
|
||||
|
||||
model SubscriptionEvent {
|
||||
id String @id @default(cuid())
|
||||
subscriptionId String
|
||||
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
||||
companyId String
|
||||
eventType String
|
||||
source String @default("system")
|
||||
payload Json @default("{}")
|
||||
occurredAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([subscriptionId])
|
||||
@@index([companyId])
|
||||
@@map("subscription_events")
|
||||
}
|
||||
|
||||
model SubscriptionInvoice {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
subscriptionId String
|
||||
subscription Subscription @relation(fields: [subscriptionId], references: [id])
|
||||
subscription Subscription @relation(fields: [subscriptionId], references: [id])
|
||||
providerInvoiceId String?
|
||||
amount Int
|
||||
currency String @default("MAD")
|
||||
currency String @default("MAD")
|
||||
status InvoiceStatus
|
||||
amanpayTransactionId String? @unique
|
||||
paypalCaptureId String? @unique
|
||||
paymentProvider PaymentProvider @default(AMANPAY)
|
||||
amanpayTransactionId String? @unique
|
||||
paypalCaptureId String? @unique
|
||||
paymentProvider PaymentProvider @default(AMANPAY)
|
||||
dueAt DateTime?
|
||||
paidAt DateTime?
|
||||
failedAt DateTime?
|
||||
voidedAt DateTime?
|
||||
attempts PaymentAttempt[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@ -398,6 +433,22 @@ model SubscriptionInvoice {
|
||||
@@map("subscription_invoices")
|
||||
}
|
||||
|
||||
model PaymentAttempt {
|
||||
id String @id @default(cuid())
|
||||
invoiceId String
|
||||
invoice SubscriptionInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
|
||||
subscriptionId String
|
||||
providerPaymentId String?
|
||||
status String
|
||||
failureCode String?
|
||||
failureMessage String?
|
||||
attemptedAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([invoiceId])
|
||||
@@map("payment_attempts")
|
||||
}
|
||||
|
||||
model BrandSettings {
|
||||
id String @id @default(cuid())
|
||||
companyId String @unique
|
||||
@@ -1115,3 +1166,16 @@ model AuditLog {
|
||||
@@index([action])
|
||||
@@map("audit_logs")
|
||||
}
|
||||
|
||||
model PricingConfig {
|
||||
id String @id @default(cuid())
|
||||
plan String
|
||||
billingPeriod String
|
||||
amount Int
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy String?
|
||||
|
||||
@@unique([plan, billingPeriod])
|
||||
@@map("pricing_configs")
|
||||
}
|
||||
|
||||
+10
-10
@@ -19,32 +19,32 @@ export interface ApiError {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
// Plan prices in smallest currency unit
|
||||
// Plan prices in smallest currency unit (MAD, in centimes)
|
||||
export const PLAN_PRICES: Record<string, Record<string, Record<string, number>>> = {
|
||||
STARTER: {
|
||||
MONTHLY: { MAD: 2990, USD: 2900, EUR: 2700 },
|
||||
ANNUAL: { MAD: 29900, USD: 29000, EUR: 27000 },
|
||||
MONTHLY: { MAD: 2990 },
|
||||
ANNUAL: { MAD: 29900 },
|
||||
},
|
||||
GROWTH: {
|
||||
MONTHLY: { MAD: 3990, USD: 5900, EUR: 5400 },
|
||||
ANNUAL: { MAD: 39900, USD: 59000, EUR: 54000 },
|
||||
MONTHLY: { MAD: 3990 },
|
||||
ANNUAL: { MAD: 39900 },
|
||||
},
|
||||
PRO: {
|
||||
MONTHLY: { MAD: 99900, USD: 9900, EUR: 9000 },
|
||||
ANNUAL: { MAD: 999000, USD: 99000, EUR: 90000 },
|
||||
MONTHLY: { MAD: 99900 },
|
||||
ANNUAL: { MAD: 999000 },
|
||||
},
|
||||
}
|
||||
|
||||
export type Locale = 'en' | 'fr' | 'ar'
|
||||
export type SupportedCurrency = 'MAD' | 'USD' | 'EUR'
|
||||
export type SupportedCurrency = 'MAD'
|
||||
|
||||
export function formatCurrency(amount: number, currency: SupportedCurrency, locale: Locale = 'en'): string {
|
||||
export function formatCurrency(amount: number, _currency: SupportedCurrency = 'MAD', locale: Locale = 'en'): string {
|
||||
const divisor = 100
|
||||
const value = amount / divisor
|
||||
const localeMap: Record<Locale, string> = { en: 'en-US', fr: 'fr-FR', ar: 'ar-MA' }
|
||||
return new Intl.NumberFormat(localeMap[locale], {
|
||||
style: 'currency',
|
||||
currency,
|
||||
currency: 'MAD',
|
||||
minimumFractionDigits: 2,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user