enum SubscriptionProductKind { base_plan recurring_add_on one_time_setup contract_override } enum TenantProductAcquisitionStatus { pending provider_pending payment_pending active failed cancelled expired refunded } model SubscriptionProduct { id String @id @default(cuid()) billingProviderId String code String? lagoPlanCode String? kind SubscriptionProductKind @default(base_plan) order Int title String active Boolean model Int public Boolean groupTitle String? groupDescription String? description String? badge String? billingAddressCollection String @default("auto") hasQuantity Boolean @default(false) canBuyAgain Boolean @default(false) prices SubscriptionPrice[] features SubscriptionFeature[] tenantProducts TenantSubscriptionProduct[] acquisitions TenantProductAcquisition[] usageBasedPrices SubscriptionUsageBasedPrice[] @@index([code]) @@index([lagoPlanCode]) } model SubscriptionPrice { id String @id @default(cuid()) subscriptionProductId String subscriptionProduct SubscriptionProduct @relation(fields: [subscriptionProductId], references: [id], onDelete: Cascade) billingProviderId String lagoPlanCode String? lagoFeeCode String? molliePriceReference String? billingCadence String @default("monthly") contractMonths Int @default(1) requiredPaymentMethod String @default("any") type Int billingPeriod Int price Decimal currency String trialDays Int active Boolean tenantProductPrices TenantSubscriptionProductPrice[] acquisitions TenantProductAcquisition[] @@index([lagoPlanCode]) @@index([lagoFeeCode]) @@index([molliePriceReference]) } model SubscriptionUsageBasedPrice { id String @id @default(cuid()) subscriptionProductId String subscriptionProduct SubscriptionProduct @relation(fields: [subscriptionProductId], references: [id], onDelete: Cascade) billingProviderId String billingPeriod Int currency String unit String unitTitle String unitTitlePlural String usageType String aggregateUsage String tiersMode String billingScheme String tiers SubscriptionUsageBasedTier[] tenantProductPrices TenantSubscriptionProductPrice[] } model SubscriptionUsageBasedTier { id String @id @default(cuid()) subscriptionUsageBasedPriceId String subscriptionUsageBasedPrice SubscriptionUsageBasedPrice @relation(fields: [subscriptionUsageBasedPriceId], references: [id], onDelete: Cascade) from Int to Int? perUnitPrice Decimal? flatFeePrice Decimal? } model SubscriptionFeature { id String @id @default(cuid()) subscriptionProductId String order Int title String name String code String? type Int value Int quotaValue Json? href String? badge String? accumulate Boolean @default(false) subscriptionProduct SubscriptionProduct @relation(fields: [subscriptionProductId], references: [id], onDelete: Cascade) @@index([code]) } model TenantSubscription { id String @id @default(cuid()) tenantId String @unique billingProviderCustomerId String? lagoCustomerId String? mollieCustomerId String? planCode String? seatQuantity Int @default(1) officeSeatQuantity Int @default(0) fieldSeatQuantity Int @default(0) billingCadence String @default("monthly") commitmentEndsAt DateTime? storageAddOnCodes String[] @default([]) lastInvoiceExternalId String? lastPaymentExternalId String? status String @default("trial") trialStartsAt DateTime? trialEndsAt DateTime? pastDueSince DateTime? frozenAt DateTime? cancelledAt DateTime? dunningAttemptCount Int @default(0) lastDunningSentAt DateTime? tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) products TenantSubscriptionProduct[] usageRecords TenantSubscriptionUsageRecord[] @@index([status]) @@index([planCode]) @@index([lagoCustomerId]) @@index([mollieCustomerId]) @@index([trialEndsAt]) @@index([pastDueSince]) @@index([frozenAt]) } model TenantSubscriptionProduct { id String @id @default(cuid()) createdAt DateTime @default(now()) tenantSubscriptionId String subscriptionProductId String customEntitlementPlanId String? cancelledAt DateTime? endsAt DateTime? billingProviderSubscriptionId String? lagoSubscriptionId String? planCode String? seatQuantity Int? officeSeatQuantity Int? fieldSeatQuantity Int? billingCadence String @default("monthly") commitmentEndsAt DateTime? storageAddOnCodes String[] @default([]) quantity Int? fromCheckoutSessionId String? currentPeriodStart DateTime? currentPeriodEnd DateTime? tenantSubscription TenantSubscription @relation(fields: [tenantSubscriptionId], references: [id], onDelete: Cascade) subscriptionProduct SubscriptionProduct @relation(fields: [subscriptionProductId], references: [id]) prices TenantSubscriptionProductPrice[] acquisition TenantProductAcquisition? @@index([lagoSubscriptionId]) @@index([planCode]) } /// Idempotent tenant-owned ledger for products acquired outside the one-base-plan /// lifecycle. `scopeKey` is the stable business scope (for example `tenant` or a /// normalized hostname) and makes provider retries safe without granting the same /// entitlement twice. Provider failures remain local to this row and must never /// mutate the base TenantSubscription status. model TenantProductAcquisition { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenantId String subscriptionProductId String subscriptionPriceId String? productKind SubscriptionProductKind scopeKey String requestHash String status TenantProductAcquisitionStatus @default(pending) version Int @default(1) quantity Int @default(1) amountCents Int? currency String? checkoutSessionId String? @unique tenantSubscriptionProductId String? @unique provider String? providerAttempt Int @default(1) providerPaymentId String? @unique lagoInvoiceIntentReference String? providerPaymentExpiresAt DateTime? providerPaidAt DateTime? invoiceExternalId String? lagoSubscriptionId String? lagoProviderSubscriptionId String? initialLagoInvoiceExternalId String? @unique checkoutUrl String? sourceReference String? failureCode String? providerStartedAt DateTime? reservationExpiresAt DateTime? activatedAt DateTime? failedAt DateTime? cancelledAt DateTime? expiredAt DateTime? refundedAt DateTime? tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) subscriptionProduct SubscriptionProduct @relation(fields: [subscriptionProductId], references: [id]) subscriptionPrice SubscriptionPrice? @relation(fields: [subscriptionPriceId], references: [id]) checkoutSession CheckoutSessionStatus? @relation(fields: [checkoutSessionId], references: [id], onDelete: SetNull) tenantSubscriptionProduct TenantSubscriptionProduct? @relation(fields: [tenantSubscriptionProductId], references: [id], onDelete: SetNull) @@unique([tenantId, subscriptionProductId, scopeKey]) @@index([tenantId, status, updatedAt]) @@index([tenantId, status, reservationExpiresAt]) @@index([subscriptionProductId, productKind, status]) @@index([invoiceExternalId]) @@index([lagoSubscriptionId]) @@index([lagoProviderSubscriptionId]) } model TenantSubscriptionProductPrice { id String @id @default(cuid()) tenantSubscriptionProductId String tenantSubscriptionProduct TenantSubscriptionProduct @relation(fields: [tenantSubscriptionProductId], references: [id], onDelete: Cascade) subscriptionPriceId String? subscriptionPrice SubscriptionPrice? @relation(fields: [subscriptionPriceId], references: [id]) subscriptionUsageBasedPriceId String? subscriptionUsageBasedPrice SubscriptionUsageBasedPrice? @relation(fields: [subscriptionUsageBasedPriceId], references: [id]) quantity Int @default(1) isMetered Boolean @default(false) } model TenantSubscriptionUsageRecord { id String @id @default(cuid()) tenantId String tenantSubscriptionId String tenantSubscriptionProductPriceId String tenantSubscription TenantSubscription @relation(fields: [tenantSubscriptionId], references: [id], onDelete: Cascade) metricCode String @default("legacy") timestamp Int quantity Int idempotencyKey String? billingProviderSubscriptionItemId String? @@unique([tenantId, metricCode, idempotencyKey]) @@index([tenantId, metricCode, timestamp]) } model TenantProductConfiguration { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenantId String @unique tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) blueprintId String @default("crm-saas") notes String? signupSubscriptionPriceId String? signupOfficeSeatQuantity Int? signupFieldSeatQuantity Int? } model TenantFeatureOverride { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenantId String tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) featureKey String override String reason String? @@unique([tenantId, featureKey]) @@index([featureKey, override]) } model CheckoutSessionStatus { id String @id createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenantId String tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) pending Boolean @default(true) // has not added products to tenant email String fromUrl String fromUserId String? createdUserId String? createdTenantId String? provider String? providerPaymentId String? invoiceExternalId String? subscriptionProductId String? subscriptionPriceId String? lagoSubscriptionId String? planCode String? productKind SubscriptionProductKind @default(base_plan) scopeKey String? quantity Int @default(1) officeSeatQuantity Int @default(0) fieldSeatQuantity Int @default(0) billingCadence String @default("monthly") commitmentEndsAt DateTime? storageAddOnCodes String[] @default([]) amountCents Int? currency String? checkoutUrl String? status String @default("open") productAcquisition TenantProductAcquisition? @@index([providerPaymentId]) @@index([tenantId, pending]) } model BillingWebhookLog { id String @id @default(cuid()) createdAt DateTime @default(now()) providerEventId String @unique eventType String processedAt DateTime? relatedTenantId String? error String? @@index([eventType, createdAt]) @@index([relatedTenantId, createdAt]) } model Credit { id String @id @default(cuid()) createdAt DateTime @default(now()) tenantId String tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) userId String? user User? @relation(fields: [userId], references: [id]) amount Int type String objectId String? @@index([tenantId, userId]) @@index([tenantId, createdAt]) }