Compare commits
3 Commits
c7e685ea08
...
feat/payme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e06bc12097 | ||
|
|
f490eecd11 | ||
|
|
ab69711e87 |
@@ -16,3 +16,23 @@ SMTP_FROM=Playzer <no-reply@example.com>
|
|||||||
# Google OAuth
|
# Google OAuth
|
||||||
GOOGLE_CLIENT_ID=your-google-client-id
|
GOOGLE_CLIENT_ID=your-google-client-id
|
||||||
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||||
|
|
||||||
|
# Billing / Subscriptions
|
||||||
|
BILLING_ENV=sandbox
|
||||||
|
FRONTEND_BASE_URL=http://localhost:5173
|
||||||
|
|
||||||
|
# Stripe
|
||||||
|
STRIPE_SECRET_KEY=sk_test_your-stripe-secret-key
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_your-webhook-secret
|
||||||
|
# Price IDs por plan y moneda (ajustar según tus products/prices en Stripe)
|
||||||
|
STRIPE_PRICE_ID_USD_BASIC=price_basic_usd
|
||||||
|
STRIPE_PRICE_ID_USD_ADVANCED=price_advanced_usd
|
||||||
|
STRIPE_PRICE_ID_USD_ENTERPRISE=price_enterprise_usd
|
||||||
|
|
||||||
|
# Mercado Pago (Argentina)
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=TEST-your-access-token
|
||||||
|
MERCADOPAGO_WEBHOOK_SECRET=your-webhook-secret
|
||||||
|
# Plan IDs por plan (ajustar según tus planes de suscripción en MP)
|
||||||
|
MERCADOPAGO_PLAN_ID_ARS_BASIC=2c938084...
|
||||||
|
MERCADOPAGO_PLAN_ID_ARS_ADVANCED=2c938084...
|
||||||
|
MERCADOPAGO_PLAN_ID_ARS_ENTERPRISE=2c938084...
|
||||||
|
|||||||
@@ -24,12 +24,14 @@
|
|||||||
"dotenv": "^17.4.1",
|
"dotenv": "^17.4.1",
|
||||||
"hono": "4.12.10",
|
"hono": "4.12.10",
|
||||||
"mailtrap": "^4.5.1",
|
"mailtrap": "^4.5.1",
|
||||||
|
"mercadopago": "^3.1.0",
|
||||||
"nodemailer": "^8.0.5",
|
"nodemailer": "^8.0.5",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
"pino": "10.3.1",
|
"pino": "10.3.1",
|
||||||
"pino-pretty": "13.1.3",
|
"pino-pretty": "13.1.3",
|
||||||
"pino-std-serializers": "7.1.0",
|
"pino-std-serializers": "7.1.0",
|
||||||
"prisma": "^7",
|
"prisma": "^7",
|
||||||
|
"stripe": "^22.2.2",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
52
apps/backend/prisma/billing.prisma
Normal file
52
apps/backend/prisma/billing.prisma
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
enum BillingProvider {
|
||||||
|
STRIPE
|
||||||
|
MERCADOPAGO
|
||||||
|
PAYPAL
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BillingStatus {
|
||||||
|
TRIAL
|
||||||
|
ACTIVE
|
||||||
|
PAST_DUE
|
||||||
|
CANCELED
|
||||||
|
SUSPENDED
|
||||||
|
}
|
||||||
|
|
||||||
|
model ComplexBilling {
|
||||||
|
complexId String @id @map("complex_id") @db.Uuid
|
||||||
|
status BillingStatus @default(TRIAL)
|
||||||
|
planCode String @map("plan_code") @db.VarChar(10)
|
||||||
|
currency String @db.VarChar(3)
|
||||||
|
provider BillingProvider?
|
||||||
|
providerCustomerId String? @map("provider_customer_id")
|
||||||
|
providerSubscriptionId String? @map("provider_subscription_id")
|
||||||
|
providerPreapprovalId String? @map("provider_preapproval_id")
|
||||||
|
currentPeriodStart DateTime? @map("current_period_start")
|
||||||
|
currentPeriodEnd DateTime? @map("current_period_end")
|
||||||
|
trialEndsAt DateTime? @map("trial_ends_at")
|
||||||
|
canceledAt DateTime? @map("canceled_at")
|
||||||
|
suspendedAt DateTime? @map("suspended_at")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
|
||||||
|
events BillingEvent[]
|
||||||
|
|
||||||
|
@@map("complex_billings")
|
||||||
|
}
|
||||||
|
|
||||||
|
model BillingEvent {
|
||||||
|
id String @id @db.Uuid
|
||||||
|
complexId String @map("complex_id") @db.Uuid
|
||||||
|
eventType String @map("event_type") @db.VarChar(100)
|
||||||
|
provider BillingProvider
|
||||||
|
providerEventId String? @unique @map("provider_event_id")
|
||||||
|
providerData Json? @map("provider_data")
|
||||||
|
previousStatus BillingStatus? @map("previous_status")
|
||||||
|
newStatus BillingStatus? @map("new_status")
|
||||||
|
processedAt DateTime @default(now()) @map("processed_at")
|
||||||
|
billing ComplexBilling @relation(fields: [complexId], references: [complexId])
|
||||||
|
|
||||||
|
@@index([provider, providerEventId])
|
||||||
|
@@index([complexId])
|
||||||
|
@@map("billing_events")
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ model Complex {
|
|||||||
invitations ComplexInvitation[]
|
invitations ComplexInvitation[]
|
||||||
courts Court[]
|
courts Court[]
|
||||||
recurringGroups RecurringBookingGroup[]
|
recurringGroups RecurringBookingGroup[]
|
||||||
|
billing ComplexBilling?
|
||||||
|
|
||||||
@@index([planCode])
|
@@index([planCode])
|
||||||
@@index([complexSlug])
|
@@index([complexSlug])
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "BillingProvider" AS ENUM ('STRIPE', 'MERCADOPAGO', 'PAYPAL');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "BillingStatus" AS ENUM ('TRIAL', 'ACTIVE', 'PAST_DUE', 'CANCELED', 'SUSPENDED');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "complex_billings" (
|
||||||
|
"complex_id" UUID NOT NULL,
|
||||||
|
"status" "BillingStatus" NOT NULL DEFAULT 'TRIAL',
|
||||||
|
"plan_code" VARCHAR(10) NOT NULL,
|
||||||
|
"currency" VARCHAR(3) NOT NULL,
|
||||||
|
"provider" "BillingProvider",
|
||||||
|
"provider_customer_id" TEXT,
|
||||||
|
"provider_subscription_id" TEXT,
|
||||||
|
"provider_preapproval_id" TEXT,
|
||||||
|
"current_period_start" TIMESTAMP(3),
|
||||||
|
"current_period_end" TIMESTAMP(3),
|
||||||
|
"trial_ends_at" TIMESTAMP(3),
|
||||||
|
"canceled_at" TIMESTAMP(3),
|
||||||
|
"suspended_at" TIMESTAMP(3),
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "complex_billings_pkey" PRIMARY KEY ("complex_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "billing_events" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"complex_id" UUID NOT NULL,
|
||||||
|
"event_type" VARCHAR(100) NOT NULL,
|
||||||
|
"provider" "BillingProvider" NOT NULL,
|
||||||
|
"provider_event_id" TEXT,
|
||||||
|
"provider_data" JSONB,
|
||||||
|
"previous_status" "BillingStatus",
|
||||||
|
"new_status" "BillingStatus",
|
||||||
|
"processed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "billing_events_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "billing_events_provider_event_id_key" ON "billing_events"("provider_event_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "billing_events_provider_provider_event_id_idx" ON "billing_events"("provider", "provider_event_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "billing_events_complex_id_idx" ON "billing_events"("complex_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "complex_billings" ADD CONSTRAINT "complex_billings_complex_id_fkey" FOREIGN KEY ("complex_id") REFERENCES "complexes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "billing_events" ADD CONSTRAINT "billing_events_complex_id_fkey" FOREIGN KEY ("complex_id") REFERENCES "complex_billings"("complex_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -37,6 +37,16 @@ export type Account = Prisma.AccountModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Verification = Prisma.VerificationModel
|
export type Verification = Prisma.VerificationModel
|
||||||
|
/**
|
||||||
|
* Model ComplexBilling
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type ComplexBilling = Prisma.ComplexBillingModel
|
||||||
|
/**
|
||||||
|
* Model BillingEvent
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type BillingEvent = Prisma.BillingEventModel
|
||||||
/**
|
/**
|
||||||
* Model Complex
|
* Model Complex
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -61,6 +61,16 @@ export type Account = Prisma.AccountModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Verification = Prisma.VerificationModel
|
export type Verification = Prisma.VerificationModel
|
||||||
|
/**
|
||||||
|
* Model ComplexBilling
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type ComplexBilling = Prisma.ComplexBillingModel
|
||||||
|
/**
|
||||||
|
* Model BillingEvent
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type BillingEvent = Prisma.BillingEventModel
|
||||||
/**
|
/**
|
||||||
* Model Complex
|
* Model Complex
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -187,6 +187,20 @@ export type UuidFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedUuidFilter<$PrismaModel> | string
|
not?: Prisma.NestedUuidFilter<$PrismaModel> | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EnumBillingStatusFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel> | $Enums.BillingStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumBillingProviderNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel> | $Enums.BillingProvider | null
|
||||||
|
}
|
||||||
|
|
||||||
export type UuidWithAggregatesFilter<$PrismaModel = never> = {
|
export type UuidWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
@@ -202,6 +216,111 @@ export type UuidWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EnumBillingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumBillingStatusWithAggregatesFilter<$PrismaModel> | $Enums.BillingStatus
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumBillingProviderNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumBillingProviderNullableWithAggregatesFilter<$PrismaModel> | $Enums.BillingProvider | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumBillingProviderFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel> | $Enums.BillingProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JsonNullableFilter<$PrismaModel = never> =
|
||||||
|
| Prisma.PatchUndefined<
|
||||||
|
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
Required<JsonNullableFilterBase<$PrismaModel>>
|
||||||
|
>
|
||||||
|
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
|
||||||
|
|
||||||
|
export type JsonNullableFilterBase<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
path?: string[]
|
||||||
|
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||||
|
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumBillingStatusNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel> | $Enums.BillingStatus | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumBillingProviderWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumBillingProviderWithAggregatesFilter<$PrismaModel> | $Enums.BillingProvider
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
|
||||||
|
| Prisma.PatchUndefined<
|
||||||
|
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
|
||||||
|
>
|
||||||
|
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
|
||||||
|
|
||||||
|
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
path?: string[]
|
||||||
|
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||||
|
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumBillingStatusNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumBillingStatusNullableWithAggregatesFilter<$PrismaModel> | $Enums.BillingStatus | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type EnumComplexUserRoleFilter<$PrismaModel = never> = {
|
export type EnumComplexUserRoleFilter<$PrismaModel = never> = {
|
||||||
equals?: $Enums.ComplexUserRole | Prisma.EnumComplexUserRoleFieldRefInput<$PrismaModel>
|
equals?: $Enums.ComplexUserRole | Prisma.EnumComplexUserRoleFieldRefInput<$PrismaModel>
|
||||||
in?: $Enums.ComplexUserRole[] | Prisma.ListEnumComplexUserRoleFieldRefInput<$PrismaModel>
|
in?: $Enums.ComplexUserRole[] | Prisma.ListEnumComplexUserRoleFieldRefInput<$PrismaModel>
|
||||||
@@ -604,6 +723,20 @@ export type NestedUuidFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedUuidFilter<$PrismaModel> | string
|
not?: Prisma.NestedUuidFilter<$PrismaModel> | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedEnumBillingStatusFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel> | $Enums.BillingStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumBillingProviderNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel> | $Enums.BillingProvider | null
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedUuidWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedUuidWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
@@ -618,6 +751,84 @@ export type NestedUuidWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedEnumBillingStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumBillingStatusWithAggregatesFilter<$PrismaModel> | $Enums.BillingStatus
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumBillingProviderNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumBillingProviderNullableWithAggregatesFilter<$PrismaModel> | $Enums.BillingProvider | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumBillingProviderFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel> | $Enums.BillingProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumBillingStatusNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel> | $Enums.BillingStatus | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumBillingProviderWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumBillingProviderWithAggregatesFilter<$PrismaModel> | $Enums.BillingProvider
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedJsonNullableFilter<$PrismaModel = never> =
|
||||||
|
| Prisma.PatchUndefined<
|
||||||
|
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
Required<NestedJsonNullableFilterBase<$PrismaModel>>
|
||||||
|
>
|
||||||
|
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
|
||||||
|
|
||||||
|
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
path?: string[]
|
||||||
|
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||||
|
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumBillingStatusNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
|
||||||
|
not?: Prisma.NestedEnumBillingStatusNullableWithAggregatesFilter<$PrismaModel> | $Enums.BillingStatus | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedEnumComplexUserRoleFilter<$PrismaModel = never> = {
|
export type NestedEnumComplexUserRoleFilter<$PrismaModel = never> = {
|
||||||
equals?: $Enums.ComplexUserRole | Prisma.EnumComplexUserRoleFieldRefInput<$PrismaModel>
|
equals?: $Enums.ComplexUserRole | Prisma.EnumComplexUserRoleFieldRefInput<$PrismaModel>
|
||||||
in?: $Enums.ComplexUserRole[] | Prisma.ListEnumComplexUserRoleFieldRefInput<$PrismaModel>
|
in?: $Enums.ComplexUserRole[] | Prisma.ListEnumComplexUserRoleFieldRefInput<$PrismaModel>
|
||||||
|
|||||||
@@ -9,6 +9,26 @@
|
|||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export const BillingProvider = {
|
||||||
|
STRIPE: 'STRIPE',
|
||||||
|
MERCADOPAGO: 'MERCADOPAGO',
|
||||||
|
PAYPAL: 'PAYPAL'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type BillingProvider = (typeof BillingProvider)[keyof typeof BillingProvider]
|
||||||
|
|
||||||
|
|
||||||
|
export const BillingStatus = {
|
||||||
|
TRIAL: 'TRIAL',
|
||||||
|
ACTIVE: 'ACTIVE',
|
||||||
|
PAST_DUE: 'PAST_DUE',
|
||||||
|
CANCELED: 'CANCELED',
|
||||||
|
SUSPENDED: 'SUSPENDED'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type BillingStatus = (typeof BillingStatus)[keyof typeof BillingStatus]
|
||||||
|
|
||||||
|
|
||||||
export const ComplexUserRole = {
|
export const ComplexUserRole = {
|
||||||
ADMIN: 'ADMIN',
|
ADMIN: 'ADMIN',
|
||||||
EMPLOYEE: 'EMPLOYEE'
|
EMPLOYEE: 'EMPLOYEE'
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -388,6 +388,8 @@ export const ModelName = {
|
|||||||
Session: 'Session',
|
Session: 'Session',
|
||||||
Account: 'Account',
|
Account: 'Account',
|
||||||
Verification: 'Verification',
|
Verification: 'Verification',
|
||||||
|
ComplexBilling: 'ComplexBilling',
|
||||||
|
BillingEvent: 'BillingEvent',
|
||||||
Complex: 'Complex',
|
Complex: 'Complex',
|
||||||
ComplexUser: 'ComplexUser',
|
ComplexUser: 'ComplexUser',
|
||||||
ComplexInvitation: 'ComplexInvitation',
|
ComplexInvitation: 'ComplexInvitation',
|
||||||
@@ -416,7 +418,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtMaintenance" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "recurringBookingGroup" | "passwordResetRequest" | "plan"
|
modelProps: "user" | "session" | "account" | "verification" | "complexBilling" | "billingEvent" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtMaintenance" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "recurringBookingGroup" | "passwordResetRequest" | "plan"
|
||||||
txIsolationLevel: TransactionIsolationLevel
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
@@ -716,6 +718,154 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ComplexBilling: {
|
||||||
|
payload: Prisma.$ComplexBillingPayload<ExtArgs>
|
||||||
|
fields: Prisma.ComplexBillingFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.ComplexBillingFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.ComplexBillingFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.ComplexBillingFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.ComplexBillingFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.ComplexBillingFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.ComplexBillingCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.ComplexBillingCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.ComplexBillingCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.ComplexBillingDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.ComplexBillingUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.ComplexBillingDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.ComplexBillingUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.ComplexBillingUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.ComplexBillingUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.ComplexBillingAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateComplexBilling>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.ComplexBillingGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.ComplexBillingGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.ComplexBillingCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.ComplexBillingCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BillingEvent: {
|
||||||
|
payload: Prisma.$BillingEventPayload<ExtArgs>
|
||||||
|
fields: Prisma.BillingEventFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.BillingEventFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.BillingEventFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.BillingEventFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.BillingEventFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.BillingEventFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.BillingEventCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.BillingEventCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.BillingEventCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.BillingEventDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.BillingEventUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.BillingEventDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.BillingEventUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.BillingEventUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.BillingEventUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.BillingEventAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateBillingEvent>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.BillingEventGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.BillingEventGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.BillingEventCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.BillingEventCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Complex: {
|
Complex: {
|
||||||
payload: Prisma.$ComplexPayload<ExtArgs>
|
payload: Prisma.$ComplexPayload<ExtArgs>
|
||||||
fields: Prisma.ComplexFieldRefs
|
fields: Prisma.ComplexFieldRefs
|
||||||
@@ -1785,6 +1935,42 @@ export const VerificationScalarFieldEnum = {
|
|||||||
export type VerificationScalarFieldEnum = (typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum]
|
export type VerificationScalarFieldEnum = (typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ComplexBillingScalarFieldEnum = {
|
||||||
|
complexId: 'complexId',
|
||||||
|
status: 'status',
|
||||||
|
planCode: 'planCode',
|
||||||
|
currency: 'currency',
|
||||||
|
provider: 'provider',
|
||||||
|
providerCustomerId: 'providerCustomerId',
|
||||||
|
providerSubscriptionId: 'providerSubscriptionId',
|
||||||
|
providerPreapprovalId: 'providerPreapprovalId',
|
||||||
|
currentPeriodStart: 'currentPeriodStart',
|
||||||
|
currentPeriodEnd: 'currentPeriodEnd',
|
||||||
|
trialEndsAt: 'trialEndsAt',
|
||||||
|
canceledAt: 'canceledAt',
|
||||||
|
suspendedAt: 'suspendedAt',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ComplexBillingScalarFieldEnum = (typeof ComplexBillingScalarFieldEnum)[keyof typeof ComplexBillingScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const BillingEventScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
complexId: 'complexId',
|
||||||
|
eventType: 'eventType',
|
||||||
|
provider: 'provider',
|
||||||
|
providerEventId: 'providerEventId',
|
||||||
|
providerData: 'providerData',
|
||||||
|
previousStatus: 'previousStatus',
|
||||||
|
newStatus: 'newStatus',
|
||||||
|
processedAt: 'processedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type BillingEventScalarFieldEnum = (typeof BillingEventScalarFieldEnum)[keyof typeof BillingEventScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const ComplexScalarFieldEnum = {
|
export const ComplexScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
complexName: 'complexName',
|
complexName: 'complexName',
|
||||||
@@ -1991,6 +2177,14 @@ export const SortOrder = {
|
|||||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||||
|
|
||||||
|
|
||||||
|
export const NullableJsonNullValueInput = {
|
||||||
|
DbNull: DbNull,
|
||||||
|
JsonNull: JsonNull
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
|
||||||
|
|
||||||
|
|
||||||
export const JsonNullValueInput = {
|
export const JsonNullValueInput = {
|
||||||
JsonNull: JsonNull
|
JsonNull: JsonNull
|
||||||
} as const
|
} as const
|
||||||
@@ -2078,6 +2272,48 @@ export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaMode
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'BillingStatus'
|
||||||
|
*/
|
||||||
|
export type EnumBillingStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BillingStatus'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'BillingStatus[]'
|
||||||
|
*/
|
||||||
|
export type ListEnumBillingStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BillingStatus[]'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'BillingProvider'
|
||||||
|
*/
|
||||||
|
export type EnumBillingProviderFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BillingProvider'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'BillingProvider[]'
|
||||||
|
*/
|
||||||
|
export type ListEnumBillingProviderFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BillingProvider[]'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'Json'
|
||||||
|
*/
|
||||||
|
export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Json'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'QueryMode'
|
||||||
|
*/
|
||||||
|
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to a field of type 'ComplexUserRole'
|
* Reference to a field of type 'ComplexUserRole'
|
||||||
*/
|
*/
|
||||||
@@ -2161,20 +2397,6 @@ export type EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel> = FieldRe
|
|||||||
export type ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RecurringBookingGroupStatus[]'>
|
export type ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RecurringBookingGroupStatus[]'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reference to a field of type 'Json'
|
|
||||||
*/
|
|
||||||
export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Json'>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reference to a field of type 'QueryMode'
|
|
||||||
*/
|
|
||||||
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch Payload for updateMany & deleteMany & createMany
|
* Batch Payload for updateMany & deleteMany & createMany
|
||||||
*/
|
*/
|
||||||
@@ -2274,6 +2496,8 @@ export type GlobalOmitConfig = {
|
|||||||
session?: Prisma.SessionOmit
|
session?: Prisma.SessionOmit
|
||||||
account?: Prisma.AccountOmit
|
account?: Prisma.AccountOmit
|
||||||
verification?: Prisma.VerificationOmit
|
verification?: Prisma.VerificationOmit
|
||||||
|
complexBilling?: Prisma.ComplexBillingOmit
|
||||||
|
billingEvent?: Prisma.BillingEventOmit
|
||||||
complex?: Prisma.ComplexOmit
|
complex?: Prisma.ComplexOmit
|
||||||
complexUser?: Prisma.ComplexUserOmit
|
complexUser?: Prisma.ComplexUserOmit
|
||||||
complexInvitation?: Prisma.ComplexInvitationOmit
|
complexInvitation?: Prisma.ComplexInvitationOmit
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export const ModelName = {
|
|||||||
Session: 'Session',
|
Session: 'Session',
|
||||||
Account: 'Account',
|
Account: 'Account',
|
||||||
Verification: 'Verification',
|
Verification: 'Verification',
|
||||||
|
ComplexBilling: 'ComplexBilling',
|
||||||
|
BillingEvent: 'BillingEvent',
|
||||||
Complex: 'Complex',
|
Complex: 'Complex',
|
||||||
ComplexUser: 'ComplexUser',
|
ComplexUser: 'ComplexUser',
|
||||||
ComplexInvitation: 'ComplexInvitation',
|
ComplexInvitation: 'ComplexInvitation',
|
||||||
@@ -154,6 +156,42 @@ export const VerificationScalarFieldEnum = {
|
|||||||
export type VerificationScalarFieldEnum = (typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum]
|
export type VerificationScalarFieldEnum = (typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ComplexBillingScalarFieldEnum = {
|
||||||
|
complexId: 'complexId',
|
||||||
|
status: 'status',
|
||||||
|
planCode: 'planCode',
|
||||||
|
currency: 'currency',
|
||||||
|
provider: 'provider',
|
||||||
|
providerCustomerId: 'providerCustomerId',
|
||||||
|
providerSubscriptionId: 'providerSubscriptionId',
|
||||||
|
providerPreapprovalId: 'providerPreapprovalId',
|
||||||
|
currentPeriodStart: 'currentPeriodStart',
|
||||||
|
currentPeriodEnd: 'currentPeriodEnd',
|
||||||
|
trialEndsAt: 'trialEndsAt',
|
||||||
|
canceledAt: 'canceledAt',
|
||||||
|
suspendedAt: 'suspendedAt',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ComplexBillingScalarFieldEnum = (typeof ComplexBillingScalarFieldEnum)[keyof typeof ComplexBillingScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const BillingEventScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
complexId: 'complexId',
|
||||||
|
eventType: 'eventType',
|
||||||
|
provider: 'provider',
|
||||||
|
providerEventId: 'providerEventId',
|
||||||
|
providerData: 'providerData',
|
||||||
|
previousStatus: 'previousStatus',
|
||||||
|
newStatus: 'newStatus',
|
||||||
|
processedAt: 'processedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type BillingEventScalarFieldEnum = (typeof BillingEventScalarFieldEnum)[keyof typeof BillingEventScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const ComplexScalarFieldEnum = {
|
export const ComplexScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
complexName: 'complexName',
|
complexName: 'complexName',
|
||||||
@@ -360,6 +398,14 @@ export const SortOrder = {
|
|||||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||||
|
|
||||||
|
|
||||||
|
export const NullableJsonNullValueInput = {
|
||||||
|
DbNull: DbNull,
|
||||||
|
JsonNull: JsonNull
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
|
||||||
|
|
||||||
|
|
||||||
export const JsonNullValueInput = {
|
export const JsonNullValueInput = {
|
||||||
JsonNull: JsonNull
|
JsonNull: JsonNull
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export type * from './models/User'
|
|||||||
export type * from './models/Session'
|
export type * from './models/Session'
|
||||||
export type * from './models/Account'
|
export type * from './models/Account'
|
||||||
export type * from './models/Verification'
|
export type * from './models/Verification'
|
||||||
|
export type * from './models/ComplexBilling'
|
||||||
|
export type * from './models/BillingEvent'
|
||||||
export type * from './models/Complex'
|
export type * from './models/Complex'
|
||||||
export type * from './models/ComplexUser'
|
export type * from './models/ComplexUser'
|
||||||
export type * from './models/ComplexInvitation'
|
export type * from './models/ComplexInvitation'
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export type ComplexWhereInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationListRelationFilter
|
invitations?: Prisma.ComplexInvitationListRelationFilter
|
||||||
courts?: Prisma.CourtListRelationFilter
|
courts?: Prisma.CourtListRelationFilter
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||||
|
billing?: Prisma.XOR<Prisma.ComplexBillingNullableScalarRelationFilter, Prisma.ComplexBillingWhereInput> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexOrderByWithRelationInput = {
|
export type ComplexOrderByWithRelationInput = {
|
||||||
@@ -254,6 +255,7 @@ export type ComplexOrderByWithRelationInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationOrderByRelationAggregateInput
|
invitations?: Prisma.ComplexInvitationOrderByRelationAggregateInput
|
||||||
courts?: Prisma.CourtOrderByRelationAggregateInput
|
courts?: Prisma.CourtOrderByRelationAggregateInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
|
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
|
||||||
|
billing?: Prisma.ComplexBillingOrderByWithRelationInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexWhereUniqueInput = Prisma.AtLeast<{
|
export type ComplexWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -276,6 +278,7 @@ export type ComplexWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
invitations?: Prisma.ComplexInvitationListRelationFilter
|
invitations?: Prisma.ComplexInvitationListRelationFilter
|
||||||
courts?: Prisma.CourtListRelationFilter
|
courts?: Prisma.CourtListRelationFilter
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
|
||||||
|
billing?: Prisma.XOR<Prisma.ComplexBillingNullableScalarRelationFilter, Prisma.ComplexBillingWhereInput> | null
|
||||||
}, "id" | "complexSlug">
|
}, "id" | "complexSlug">
|
||||||
|
|
||||||
export type ComplexOrderByWithAggregationInput = {
|
export type ComplexOrderByWithAggregationInput = {
|
||||||
@@ -328,6 +331,7 @@ export type ComplexCreateInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateInput = {
|
export type ComplexUncheckedCreateInput = {
|
||||||
@@ -346,6 +350,7 @@ export type ComplexUncheckedCreateInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUpdateInput = {
|
export type ComplexUpdateInput = {
|
||||||
@@ -364,6 +369,7 @@ export type ComplexUpdateInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateInput = {
|
export type ComplexUncheckedUpdateInput = {
|
||||||
@@ -382,6 +388,7 @@ export type ComplexUncheckedUpdateInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateManyInput = {
|
export type ComplexCreateManyInput = {
|
||||||
@@ -425,6 +432,11 @@ export type ComplexUncheckedUpdateManyInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ComplexScalarRelationFilter = {
|
||||||
|
is?: Prisma.ComplexWhereInput
|
||||||
|
isNot?: Prisma.ComplexWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
export type ComplexCountOrderByAggregateInput = {
|
export type ComplexCountOrderByAggregateInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
complexName?: Prisma.SortOrder
|
complexName?: Prisma.SortOrder
|
||||||
@@ -467,11 +479,6 @@ export type ComplexMinOrderByAggregateInput = {
|
|||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexScalarRelationFilter = {
|
|
||||||
is?: Prisma.ComplexWhereInput
|
|
||||||
isNot?: Prisma.ComplexWhereInput
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ComplexListRelationFilter = {
|
export type ComplexListRelationFilter = {
|
||||||
every?: Prisma.ComplexWhereInput
|
every?: Prisma.ComplexWhereInput
|
||||||
some?: Prisma.ComplexWhereInput
|
some?: Prisma.ComplexWhereInput
|
||||||
@@ -482,6 +489,20 @@ export type ComplexOrderByRelationAggregateInput = {
|
|||||||
_count?: Prisma.SortOrder
|
_count?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ComplexCreateNestedOneWithoutBillingInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.ComplexCreateWithoutBillingInput, Prisma.ComplexUncheckedCreateWithoutBillingInput>
|
||||||
|
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutBillingInput
|
||||||
|
connect?: Prisma.ComplexWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUpdateOneRequiredWithoutBillingNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.ComplexCreateWithoutBillingInput, Prisma.ComplexUncheckedCreateWithoutBillingInput>
|
||||||
|
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutBillingInput
|
||||||
|
upsert?: Prisma.ComplexUpsertWithoutBillingInput
|
||||||
|
connect?: Prisma.ComplexWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutBillingInput, Prisma.ComplexUpdateWithoutBillingInput>, Prisma.ComplexUncheckedUpdateWithoutBillingInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type ComplexCreateNestedOneWithoutUsersInput = {
|
export type ComplexCreateNestedOneWithoutUsersInput = {
|
||||||
create?: Prisma.XOR<Prisma.ComplexCreateWithoutUsersInput, Prisma.ComplexUncheckedCreateWithoutUsersInput>
|
create?: Prisma.XOR<Prisma.ComplexCreateWithoutUsersInput, Prisma.ComplexUncheckedCreateWithoutUsersInput>
|
||||||
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutUsersInput
|
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutUsersInput
|
||||||
@@ -580,6 +601,94 @@ export type ComplexUncheckedUpdateManyWithoutPlanNestedInput = {
|
|||||||
deleteMany?: Prisma.ComplexScalarWhereInput | Prisma.ComplexScalarWhereInput[]
|
deleteMany?: Prisma.ComplexScalarWhereInput | Prisma.ComplexScalarWhereInput[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ComplexCreateWithoutBillingInput = {
|
||||||
|
id: string
|
||||||
|
complexName: string
|
||||||
|
physicalAddress?: string | null
|
||||||
|
city?: string | null
|
||||||
|
state?: string | null
|
||||||
|
country?: string | null
|
||||||
|
complexSlug: string
|
||||||
|
adminEmail: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
|
||||||
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUncheckedCreateWithoutBillingInput = {
|
||||||
|
id: string
|
||||||
|
complexName: string
|
||||||
|
physicalAddress?: string | null
|
||||||
|
city?: string | null
|
||||||
|
state?: string | null
|
||||||
|
country?: string | null
|
||||||
|
complexSlug: string
|
||||||
|
adminEmail: string
|
||||||
|
planCode?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexCreateOrConnectWithoutBillingInput = {
|
||||||
|
where: Prisma.ComplexWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.ComplexCreateWithoutBillingInput, Prisma.ComplexUncheckedCreateWithoutBillingInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUpsertWithoutBillingInput = {
|
||||||
|
update: Prisma.XOR<Prisma.ComplexUpdateWithoutBillingInput, Prisma.ComplexUncheckedUpdateWithoutBillingInput>
|
||||||
|
create: Prisma.XOR<Prisma.ComplexCreateWithoutBillingInput, Prisma.ComplexUncheckedCreateWithoutBillingInput>
|
||||||
|
where?: Prisma.ComplexWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUpdateToOneWithWhereWithoutBillingInput = {
|
||||||
|
where?: Prisma.ComplexWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.ComplexUpdateWithoutBillingInput, Prisma.ComplexUncheckedUpdateWithoutBillingInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUpdateWithoutBillingInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
complexName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
|
||||||
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComplexUncheckedUpdateWithoutBillingInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
complexName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
planCode?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
export type ComplexCreateWithoutUsersInput = {
|
export type ComplexCreateWithoutUsersInput = {
|
||||||
id: string
|
id: string
|
||||||
complexName: string
|
complexName: string
|
||||||
@@ -595,6 +704,7 @@ export type ComplexCreateWithoutUsersInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutUsersInput = {
|
export type ComplexUncheckedCreateWithoutUsersInput = {
|
||||||
@@ -612,6 +722,7 @@ export type ComplexUncheckedCreateWithoutUsersInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutUsersInput = {
|
export type ComplexCreateOrConnectWithoutUsersInput = {
|
||||||
@@ -645,6 +756,7 @@ export type ComplexUpdateWithoutUsersInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutUsersInput = {
|
export type ComplexUncheckedUpdateWithoutUsersInput = {
|
||||||
@@ -662,6 +774,7 @@ export type ComplexUncheckedUpdateWithoutUsersInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateWithoutInvitationsInput = {
|
export type ComplexCreateWithoutInvitationsInput = {
|
||||||
@@ -679,6 +792,7 @@ export type ComplexCreateWithoutInvitationsInput = {
|
|||||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutInvitationsInput = {
|
export type ComplexUncheckedCreateWithoutInvitationsInput = {
|
||||||
@@ -696,6 +810,7 @@ export type ComplexUncheckedCreateWithoutInvitationsInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutInvitationsInput = {
|
export type ComplexCreateOrConnectWithoutInvitationsInput = {
|
||||||
@@ -729,6 +844,7 @@ export type ComplexUpdateWithoutInvitationsInput = {
|
|||||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutInvitationsInput = {
|
export type ComplexUncheckedUpdateWithoutInvitationsInput = {
|
||||||
@@ -746,6 +862,7 @@ export type ComplexUncheckedUpdateWithoutInvitationsInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateWithoutCourtsInput = {
|
export type ComplexCreateWithoutCourtsInput = {
|
||||||
@@ -763,6 +880,7 @@ export type ComplexCreateWithoutCourtsInput = {
|
|||||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutCourtsInput = {
|
export type ComplexUncheckedCreateWithoutCourtsInput = {
|
||||||
@@ -780,6 +898,7 @@ export type ComplexUncheckedCreateWithoutCourtsInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutCourtsInput = {
|
export type ComplexCreateOrConnectWithoutCourtsInput = {
|
||||||
@@ -813,6 +932,7 @@ export type ComplexUpdateWithoutCourtsInput = {
|
|||||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutCourtsInput = {
|
export type ComplexUncheckedUpdateWithoutCourtsInput = {
|
||||||
@@ -830,6 +950,7 @@ export type ComplexUncheckedUpdateWithoutCourtsInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateWithoutRecurringGroupsInput = {
|
export type ComplexCreateWithoutRecurringGroupsInput = {
|
||||||
@@ -847,6 +968,7 @@ export type ComplexCreateWithoutRecurringGroupsInput = {
|
|||||||
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutRecurringGroupsInput = {
|
export type ComplexUncheckedCreateWithoutRecurringGroupsInput = {
|
||||||
@@ -864,6 +986,7 @@ export type ComplexUncheckedCreateWithoutRecurringGroupsInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutRecurringGroupsInput = {
|
export type ComplexCreateOrConnectWithoutRecurringGroupsInput = {
|
||||||
@@ -897,6 +1020,7 @@ export type ComplexUpdateWithoutRecurringGroupsInput = {
|
|||||||
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutRecurringGroupsInput = {
|
export type ComplexUncheckedUpdateWithoutRecurringGroupsInput = {
|
||||||
@@ -914,6 +1038,7 @@ export type ComplexUncheckedUpdateWithoutRecurringGroupsInput = {
|
|||||||
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateWithoutPlanInput = {
|
export type ComplexCreateWithoutPlanInput = {
|
||||||
@@ -931,6 +1056,7 @@ export type ComplexCreateWithoutPlanInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedCreateWithoutPlanInput = {
|
export type ComplexUncheckedCreateWithoutPlanInput = {
|
||||||
@@ -948,6 +1074,7 @@ export type ComplexUncheckedCreateWithoutPlanInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
|
||||||
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexCreateOrConnectWithoutPlanInput = {
|
export type ComplexCreateOrConnectWithoutPlanInput = {
|
||||||
@@ -1021,6 +1148,7 @@ export type ComplexUpdateWithoutPlanInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateWithoutPlanInput = {
|
export type ComplexUncheckedUpdateWithoutPlanInput = {
|
||||||
@@ -1038,6 +1166,7 @@ export type ComplexUncheckedUpdateWithoutPlanInput = {
|
|||||||
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
|
||||||
|
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComplexUncheckedUpdateManyWithoutPlanInput = {
|
export type ComplexUncheckedUpdateManyWithoutPlanInput = {
|
||||||
@@ -1128,6 +1257,7 @@ export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
||||||
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
||||||
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
|
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
|
||||||
|
billing?: boolean | Prisma.Complex$billingArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["complex"]>
|
}, ExtArgs["result"]["complex"]>
|
||||||
|
|
||||||
@@ -1182,6 +1312,7 @@ export type ComplexInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
|
||||||
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
|
||||||
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
|
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
|
||||||
|
billing?: boolean | Prisma.Complex$billingArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type ComplexIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type ComplexIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
@@ -1199,6 +1330,7 @@ export type $ComplexPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
|
|||||||
invitations: Prisma.$ComplexInvitationPayload<ExtArgs>[]
|
invitations: Prisma.$ComplexInvitationPayload<ExtArgs>[]
|
||||||
courts: Prisma.$CourtPayload<ExtArgs>[]
|
courts: Prisma.$CourtPayload<ExtArgs>[]
|
||||||
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
|
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
|
||||||
|
billing: Prisma.$ComplexBillingPayload<ExtArgs> | null
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -1611,6 +1743,7 @@ export interface Prisma__ComplexClient<T, Null = never, ExtArgs extends runtime.
|
|||||||
invitations<T extends Prisma.Complex$invitationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$invitationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexInvitationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
invitations<T extends Prisma.Complex$invitationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$invitationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexInvitationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
courts<T extends Prisma.Complex$courtsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$courtsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
courts<T extends Prisma.Complex$courtsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$courtsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
recurringGroups<T extends Prisma.Complex$recurringGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$recurringGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
recurringGroups<T extends Prisma.Complex$recurringGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$recurringGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
billing<T extends Prisma.Complex$billingArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$billingArgs<ExtArgs>>): Prisma.Prisma__ComplexBillingClient<runtime.Types.Result.GetResult<Prisma.$ComplexBillingPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -2166,6 +2299,25 @@ export type Complex$recurringGroupsArgs<ExtArgs extends runtime.Types.Extensions
|
|||||||
distinct?: Prisma.RecurringBookingGroupScalarFieldEnum | Prisma.RecurringBookingGroupScalarFieldEnum[]
|
distinct?: Prisma.RecurringBookingGroupScalarFieldEnum | Prisma.RecurringBookingGroupScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complex.billing
|
||||||
|
*/
|
||||||
|
export type Complex$billingArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the ComplexBilling
|
||||||
|
*/
|
||||||
|
select?: Prisma.ComplexBillingSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the ComplexBilling
|
||||||
|
*/
|
||||||
|
omit?: Prisma.ComplexBillingOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.ComplexBillingInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.ComplexBillingWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complex without action
|
* Complex without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ export async function createAdminRecurringBooking(
|
|||||||
customerEmail: b.customerEmail,
|
customerEmail: b.customerEmail,
|
||||||
price: 0,
|
price: 0,
|
||||||
status: b.status as 'CONFIRMED',
|
status: b.status as 'CONFIRMED',
|
||||||
recurringGroupId: groupId,
|
recurringGroupId: result.group.id,
|
||||||
createdAt: b.createdAt.toISOString(),
|
createdAt: b.createdAt.toISOString(),
|
||||||
updatedAt: b.updatedAt.toISOString(),
|
updatedAt: b.updatedAt.toISOString(),
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export async function ensureComplexAccess(
|
|||||||
Result<{
|
Result<{
|
||||||
id: string;
|
id: string;
|
||||||
complexName: string;
|
complexName: string;
|
||||||
plan: { rules: string } | null;
|
plan: { rules: unknown } | null;
|
||||||
}>
|
}>
|
||||||
> {
|
> {
|
||||||
const complexUser = await db.complexUser.findUnique({
|
const complexUser = await db.complexUser.findUnique({
|
||||||
|
|||||||
24
apps/backend/src/modules/billing/billing.routes.ts
Normal file
24
apps/backend/src/modules/billing/billing.routes.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { validate } from '@/lib/http/validate';
|
||||||
|
import { requireAuth } from '@/middlewares/require-auth.middleware';
|
||||||
|
import { cancelSubscriptionHandler } from '@/modules/billing/features/cancel-subscription/cancel-subscription.handler';
|
||||||
|
import { createCheckoutHandler } from '@/modules/billing/features/create-checkout/create-checkout.handler';
|
||||||
|
import { getBillingStatusHandler } from '@/modules/billing/features/get-billing-status/get-billing-status.handler';
|
||||||
|
import { mercadopagoWebhookHandler } from '@/modules/billing/features/mercadopago-webhook/mercadopago-webhook.handler';
|
||||||
|
import { stripeWebhookHandler } from '@/modules/billing/features/stripe-webhook/stripe-webhook.handler';
|
||||||
|
import type { AppEnv } from '@/types/hono';
|
||||||
|
import { createCheckoutSchema } from '@repo/api-contract';
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
|
export const billingRoutes = new Hono<AppEnv>();
|
||||||
|
|
||||||
|
billingRoutes.post(
|
||||||
|
'/checkout',
|
||||||
|
requireAuth,
|
||||||
|
validate.json(createCheckoutSchema),
|
||||||
|
createCheckoutHandler
|
||||||
|
);
|
||||||
|
billingRoutes.post('/cancel', requireAuth, cancelSubscriptionHandler);
|
||||||
|
billingRoutes.get('/status', requireAuth, getBillingStatusHandler);
|
||||||
|
|
||||||
|
billingRoutes.post('/webhooks/stripe', stripeWebhookHandler);
|
||||||
|
billingRoutes.post('/webhooks/mercadopago', mercadopagoWebhookHandler);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
import { getBilling } from '@/modules/billing/services/billing-repository.service';
|
||||||
|
import { getProviderForComplex } from '@/modules/billing/services/provider-resolver.service';
|
||||||
|
|
||||||
|
export async function cancelSubscription(
|
||||||
|
user: { id: string },
|
||||||
|
complexId: string
|
||||||
|
): Promise<Result<{ status: string }>> {
|
||||||
|
const userAccess = await db.complexUser.findUnique({
|
||||||
|
where: { complexId_userId: { complexId, userId: user.id } },
|
||||||
|
select: { role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userAccess || userAccess.role !== 'ADMIN') {
|
||||||
|
return err(Errors.forbidden('Only admins can cancel the subscription'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const billingResult = await getBilling(complexId);
|
||||||
|
if (!billingResult.ok) return err(billingResult.error);
|
||||||
|
const billing = billingResult.value;
|
||||||
|
|
||||||
|
if (billing.status === 'CANCELED' || billing.status === 'SUSPENDED') {
|
||||||
|
return err(Errors.conflict('La suscripción ya está cancelada o suspendida'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerResult = await getProviderForComplex(complexId);
|
||||||
|
if (!providerResult.ok) return err(providerResult.error);
|
||||||
|
const { provider } = providerResult.value;
|
||||||
|
|
||||||
|
const subId = billing.providerSubscriptionId;
|
||||||
|
if (subId) {
|
||||||
|
const cancelResult = await provider.cancelSubscription(subId);
|
||||||
|
if (!cancelResult.ok) return err(cancelResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.complexBilling.update({
|
||||||
|
where: { complexId },
|
||||||
|
data: {
|
||||||
|
status: 'CANCELED',
|
||||||
|
canceledAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok({ status: 'CANCELED' });
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { getCookie } from 'hono/cookie';
|
||||||
|
import { cancelSubscription } from './cancel-subscription.business';
|
||||||
|
|
||||||
|
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
|
||||||
|
|
||||||
|
export async function cancelSubscriptionHandler(c: AppContext) {
|
||||||
|
const user = c.get('user');
|
||||||
|
const complexId = getCookie(c, SELECTED_COMPLEX_COOKIE);
|
||||||
|
|
||||||
|
if (!complexId) {
|
||||||
|
return c.json({ message: 'No complex selected' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleResult(c, await cancelSubscription(user, complexId));
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
import { hasActiveSubscription } from '@/modules/billing/services/billing-repository.service';
|
||||||
|
import { getProviderForComplex } from '@/modules/billing/services/provider-resolver.service';
|
||||||
|
import type { CheckoutResponse, CreateCheckoutInput } from '@repo/api-contract';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
|
export async function createCheckout(
|
||||||
|
user: { id: string; email: string },
|
||||||
|
complexId: string,
|
||||||
|
input: CreateCheckoutInput
|
||||||
|
): Promise<Result<CheckoutResponse>> {
|
||||||
|
const complex = await db.complex.findUnique({
|
||||||
|
where: { id: complexId },
|
||||||
|
select: { id: true, country: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!complex) {
|
||||||
|
return err(Errors.notFound('Complex not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAccess = await db.complexUser.findUnique({
|
||||||
|
where: { complexId_userId: { complexId, userId: user.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userAccess) {
|
||||||
|
return err(Errors.forbidden('You do not have access to this complex'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeResult = await hasActiveSubscription(complexId);
|
||||||
|
if (!activeResult.ok) return err(activeResult.error);
|
||||||
|
if (activeResult.value) {
|
||||||
|
return err(Errors.conflict('Ya existe una suscripción activa para este complejo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerResult = await getProviderForComplex(complexId);
|
||||||
|
if (!providerResult.ok) return err(providerResult.error);
|
||||||
|
const { provider, country } = providerResult.value;
|
||||||
|
|
||||||
|
const currency = country === 'AR' ? 'ARS' : 'USD';
|
||||||
|
|
||||||
|
const baseUrl = Bun.env.FRONTEND_BASE_URL || Bun.env.APP_BASE_URL || 'http://localhost:5173';
|
||||||
|
const successUrl = `${baseUrl}/billing/success`;
|
||||||
|
const cancelUrl = `${baseUrl}/billing/cancel`;
|
||||||
|
|
||||||
|
const checkoutResult = await provider.createCheckout({
|
||||||
|
planCode: input.planCode,
|
||||||
|
complexId,
|
||||||
|
customerEmail: user.email,
|
||||||
|
successUrl,
|
||||||
|
cancelUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!checkoutResult.ok) return err(checkoutResult.error);
|
||||||
|
|
||||||
|
await db.complexBilling.upsert({
|
||||||
|
where: { complexId },
|
||||||
|
create: {
|
||||||
|
complexId,
|
||||||
|
planCode: input.planCode,
|
||||||
|
currency,
|
||||||
|
status: 'TRIAL',
|
||||||
|
provider: country === 'AR' ? 'MERCADOPAGO' : 'STRIPE',
|
||||||
|
providerSubscriptionId: checkoutResult.value.providerSubscriptionId,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
planCode: input.planCode,
|
||||||
|
currency,
|
||||||
|
status: 'TRIAL',
|
||||||
|
provider: country === 'AR' ? 'MERCADOPAGO' : 'STRIPE',
|
||||||
|
providerSubscriptionId: checkoutResult.value.providerSubscriptionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok({
|
||||||
|
checkoutUrl: checkoutResult.value.checkoutUrl,
|
||||||
|
subscriptionId: checkoutResult.value.providerSubscriptionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import type { CreateCheckoutInput } from '@repo/api-contract';
|
||||||
|
import { getCookie } from 'hono/cookie';
|
||||||
|
import { createCheckout } from './create-checkout.business';
|
||||||
|
|
||||||
|
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
|
||||||
|
|
||||||
|
export async function createCheckoutHandler(c: AppContext) {
|
||||||
|
const user = c.get('user');
|
||||||
|
const complexId = getCookie(c, SELECTED_COMPLEX_COOKIE);
|
||||||
|
const payload = c.req.valid('json' as never) as CreateCheckoutInput;
|
||||||
|
|
||||||
|
if (!complexId) {
|
||||||
|
return c.json({ message: 'No complex selected' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleResult(c, await createCheckout(user, complexId, payload), 201);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
import { getBilling } from '@/modules/billing/services/billing-repository.service';
|
||||||
|
import type { BillingStatusResponse } from '@repo/api-contract';
|
||||||
|
|
||||||
|
export async function getBillingStatus(
|
||||||
|
_user: { id: string },
|
||||||
|
complexId: string
|
||||||
|
): Promise<Result<BillingStatusResponse>> {
|
||||||
|
const billingResult = await getBilling(complexId);
|
||||||
|
if (!billingResult.ok) return err(billingResult.error);
|
||||||
|
const billing = billingResult.value;
|
||||||
|
|
||||||
|
return ok({
|
||||||
|
complexId: billing.complexId,
|
||||||
|
status: billing.status,
|
||||||
|
planCode: billing.planCode,
|
||||||
|
currency: billing.currency,
|
||||||
|
provider: billing.provider,
|
||||||
|
currentPeriodEnd: billing.currentPeriodEnd?.toISOString() ?? null,
|
||||||
|
currentPeriodStart: billing.currentPeriodStart?.toISOString() ?? null,
|
||||||
|
trialEndsAt: billing.trialEndsAt?.toISOString() ?? null,
|
||||||
|
canceledAt: billing.canceledAt?.toISOString() ?? null,
|
||||||
|
suspendedAt: billing.suspendedAt?.toISOString() ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { getCookie } from 'hono/cookie';
|
||||||
|
import { getBillingStatus } from './get-billing-status.business';
|
||||||
|
|
||||||
|
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
|
||||||
|
|
||||||
|
export async function getBillingStatusHandler(c: AppContext) {
|
||||||
|
const user = c.get('user');
|
||||||
|
const complexId = getCookie(c, SELECTED_COMPLEX_COOKIE);
|
||||||
|
|
||||||
|
if (!complexId) {
|
||||||
|
return c.json({ message: 'No complex selected' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleResult(c, await getBillingStatus(user, complexId));
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
import type { WebhookEvent } from '@/modules/billing/providers/billing-provider.interface';
|
||||||
|
import { mercadopagoBillingProvider } from '@/modules/billing/providers/mercadopago.provider';
|
||||||
|
import {
|
||||||
|
findEventByProviderId,
|
||||||
|
recordBillingEvent,
|
||||||
|
updateBillingStatus,
|
||||||
|
} from '@/modules/billing/services/billing-repository.service';
|
||||||
|
import type { BillingStatusEnum } from '@/modules/billing/services/billing-repository.service';
|
||||||
|
|
||||||
|
const MP_PROVIDER = 'MERCADOPAGO';
|
||||||
|
|
||||||
|
function mapWebhookTypeToStatus(event: WebhookEvent): BillingStatusEnum | null {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'activated':
|
||||||
|
return 'ACTIVE';
|
||||||
|
case 'updated':
|
||||||
|
return 'ACTIVE';
|
||||||
|
case 'payment_failed':
|
||||||
|
return 'PAST_DUE';
|
||||||
|
case 'cancelled':
|
||||||
|
return 'CANCELED';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBillingByPreapprovalId(preapprovalId: string) {
|
||||||
|
return db.complexBilling.findFirst({
|
||||||
|
where: { providerPreapprovalId: preapprovalId },
|
||||||
|
}) as Promise<{ complexId: string; status: BillingStatusEnum } | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBillingByExternalRef(externalRef: string) {
|
||||||
|
return db.complexBilling.findUnique({
|
||||||
|
where: { complexId: externalRef },
|
||||||
|
}) as Promise<{ complexId: string; status: BillingStatusEnum } | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processMercadoPagoWebhook(
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
signature: string | undefined,
|
||||||
|
requestId: string | undefined
|
||||||
|
): Promise<Result<{ received: boolean }>> {
|
||||||
|
const parsedResult = await mercadopagoBillingProvider.parseWebhook(body, signature, requestId);
|
||||||
|
|
||||||
|
if (!parsedResult.ok) {
|
||||||
|
if (parsedResult.error.type === 'validation') {
|
||||||
|
return err(parsedResult.error);
|
||||||
|
}
|
||||||
|
logger.error({ error: parsedResult.error }, 'MP webhook parse failed');
|
||||||
|
return ok({ received: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = parsedResult.value;
|
||||||
|
|
||||||
|
const existingResult = await findEventByProviderId(event.providerEventId);
|
||||||
|
if (!existingResult.ok) return err(existingResult.error);
|
||||||
|
if (existingResult.value) {
|
||||||
|
logger.info(
|
||||||
|
{ providerEventId: event.providerEventId },
|
||||||
|
'MP webhook: already processed, skipping'
|
||||||
|
);
|
||||||
|
return ok({ received: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStatus = mapWebhookTypeToStatus(event);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const billing =
|
||||||
|
(await getBillingByPreapprovalId(event.providerSubscriptionId)) ??
|
||||||
|
(await getBillingByExternalRef(event.providerSubscriptionId));
|
||||||
|
|
||||||
|
if (!billing) {
|
||||||
|
logger.info(
|
||||||
|
{ providerSubscriptionId: event.providerSubscriptionId, eventType: event.type },
|
||||||
|
'MP webhook: no billing record found'
|
||||||
|
);
|
||||||
|
await recordBillingEvent({
|
||||||
|
complexId: 'unknown',
|
||||||
|
eventType: `unlinked:${event.type}`,
|
||||||
|
provider: MP_PROVIDER,
|
||||||
|
providerEventId: event.providerEventId,
|
||||||
|
providerData: event.data ?? null,
|
||||||
|
previousStatus: null,
|
||||||
|
newStatus: null,
|
||||||
|
});
|
||||||
|
return ok({ received: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousStatus = billing.status;
|
||||||
|
|
||||||
|
if (newStatus) {
|
||||||
|
await updateBillingStatus(billing.complexId, newStatus, {
|
||||||
|
provider: MP_PROVIDER,
|
||||||
|
providerPreapprovalId: event.providerSubscriptionId,
|
||||||
|
...(event.type === 'activated' || event.type === 'updated'
|
||||||
|
? {
|
||||||
|
currentPeriodStart: new Date(),
|
||||||
|
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await recordBillingEvent({
|
||||||
|
complexId: billing.complexId,
|
||||||
|
eventType: event.type,
|
||||||
|
provider: MP_PROVIDER,
|
||||||
|
providerEventId: event.providerEventId,
|
||||||
|
providerData: event.data ?? null,
|
||||||
|
previousStatus,
|
||||||
|
newStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok({ received: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error, providerEventId: event.providerEventId }, 'MP webhook processing failed');
|
||||||
|
await recordBillingEvent({
|
||||||
|
complexId: 'unknown',
|
||||||
|
eventType: `error:${event.type}`,
|
||||||
|
provider: MP_PROVIDER,
|
||||||
|
providerEventId: event.providerEventId,
|
||||||
|
providerData: { error: String(error) },
|
||||||
|
previousStatus: null,
|
||||||
|
newStatus: null,
|
||||||
|
}).catch((e) => logger.error({ error: e }, 'Failed to record error event'));
|
||||||
|
return ok({ received: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { processMercadoPagoWebhook } from './mercadopago-webhook.business';
|
||||||
|
|
||||||
|
export async function mercadopagoWebhookHandler(c: AppContext) {
|
||||||
|
const body = await c.req.json();
|
||||||
|
const signature = c.req.header('x-signature');
|
||||||
|
const requestId = c.req.header('x-request-id');
|
||||||
|
|
||||||
|
return handleResult(
|
||||||
|
c,
|
||||||
|
await processMercadoPagoWebhook(body, signature ?? undefined, requestId ?? undefined),
|
||||||
|
200
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
import type { WebhookEvent } from '@/modules/billing/providers/billing-provider.interface';
|
||||||
|
import { stripeBillingProvider } from '@/modules/billing/providers/stripe.provider';
|
||||||
|
import {
|
||||||
|
findEventByProviderId,
|
||||||
|
recordBillingEvent,
|
||||||
|
updateBillingStatus,
|
||||||
|
} from '@/modules/billing/services/billing-repository.service';
|
||||||
|
import type { BillingStatusEnum } from '@/modules/billing/services/billing-repository.service';
|
||||||
|
|
||||||
|
const STRIPE_PROVIDER = 'STRIPE';
|
||||||
|
|
||||||
|
function mapWebhookTypeToStatus(event: WebhookEvent): BillingStatusEnum | null {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'activated':
|
||||||
|
return 'ACTIVE';
|
||||||
|
case 'updated':
|
||||||
|
return 'ACTIVE';
|
||||||
|
case 'payment_failed':
|
||||||
|
return 'PAST_DUE';
|
||||||
|
case 'cancelled':
|
||||||
|
return 'CANCELED';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBillingBySubscription(providerSubscriptionId: string) {
|
||||||
|
return db.complexBilling.findFirst({
|
||||||
|
where: { providerSubscriptionId },
|
||||||
|
}) as Promise<{ complexId: string; status: BillingStatusEnum } | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processStripeWebhook(
|
||||||
|
rawBody: string,
|
||||||
|
signature: string | undefined
|
||||||
|
): Promise<Result<{ received: boolean }>> {
|
||||||
|
const parsedResult = await stripeBillingProvider.parseWebhook(rawBody, signature);
|
||||||
|
|
||||||
|
if (!parsedResult.ok) {
|
||||||
|
if (parsedResult.error.type === 'validation') {
|
||||||
|
return err(parsedResult.error);
|
||||||
|
}
|
||||||
|
logger.error({ error: parsedResult.error }, 'Stripe webhook parse failed');
|
||||||
|
return ok({ received: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = parsedResult.value;
|
||||||
|
|
||||||
|
const existingResult = await findEventByProviderId(event.providerEventId);
|
||||||
|
if (!existingResult.ok) return err(existingResult.error);
|
||||||
|
if (existingResult.value) {
|
||||||
|
logger.info(
|
||||||
|
{ providerEventId: event.providerEventId },
|
||||||
|
'Stripe webhook: already processed, skipping'
|
||||||
|
);
|
||||||
|
return ok({ received: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStatus = mapWebhookTypeToStatus(event);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const billing = await getBillingBySubscription(event.providerSubscriptionId);
|
||||||
|
|
||||||
|
if (!billing) {
|
||||||
|
logger.info(
|
||||||
|
{ providerSubscriptionId: event.providerSubscriptionId, eventType: event.type },
|
||||||
|
'Stripe webhook: no billing record found for subscription'
|
||||||
|
);
|
||||||
|
await recordBillingEvent({
|
||||||
|
complexId: 'unknown',
|
||||||
|
eventType: `unlinked:${event.type}`,
|
||||||
|
provider: STRIPE_PROVIDER,
|
||||||
|
providerEventId: event.providerEventId,
|
||||||
|
providerData: event.data ?? null,
|
||||||
|
previousStatus: null,
|
||||||
|
newStatus: null,
|
||||||
|
});
|
||||||
|
return ok({ received: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousStatus = billing.status;
|
||||||
|
|
||||||
|
if (newStatus) {
|
||||||
|
await updateBillingStatus(billing.complexId, newStatus, {
|
||||||
|
provider: STRIPE_PROVIDER,
|
||||||
|
providerCustomerId: event.providerCustomerId,
|
||||||
|
providerSubscriptionId: event.providerSubscriptionId,
|
||||||
|
...(event.type === 'activated' || event.type === 'updated'
|
||||||
|
? {
|
||||||
|
currentPeriodStart: new Date(),
|
||||||
|
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await recordBillingEvent({
|
||||||
|
complexId: billing.complexId,
|
||||||
|
eventType: event.type,
|
||||||
|
provider: STRIPE_PROVIDER,
|
||||||
|
providerEventId: event.providerEventId,
|
||||||
|
providerData: event.data ?? null,
|
||||||
|
previousStatus,
|
||||||
|
newStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok({ received: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ error, providerEventId: event.providerEventId },
|
||||||
|
'Stripe webhook processing failed'
|
||||||
|
);
|
||||||
|
await recordBillingEvent({
|
||||||
|
complexId: 'unknown',
|
||||||
|
eventType: `error:${event.type}`,
|
||||||
|
provider: STRIPE_PROVIDER,
|
||||||
|
providerEventId: event.providerEventId,
|
||||||
|
providerData: { error: String(error) },
|
||||||
|
previousStatus: null,
|
||||||
|
newStatus: null,
|
||||||
|
}).catch((e) => logger.error({ error: e }, 'Failed to record error event'));
|
||||||
|
return ok({ received: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { handleResult } from '@/lib/http/handle-result';
|
||||||
|
import type { AppContext } from '@/types/hono';
|
||||||
|
import { processStripeWebhook } from './stripe-webhook.business';
|
||||||
|
|
||||||
|
export async function stripeWebhookHandler(c: AppContext) {
|
||||||
|
const body = await c.req.raw.text();
|
||||||
|
const signature = c.req.header('stripe-signature');
|
||||||
|
|
||||||
|
return handleResult(c, await processStripeWebhook(body, signature ?? undefined), 200);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
|
||||||
|
export type WebhookEventType = 'activated' | 'updated' | 'cancelled' | 'payment_failed';
|
||||||
|
|
||||||
|
export type WebhookEvent = {
|
||||||
|
providerEventId: string;
|
||||||
|
type: WebhookEventType;
|
||||||
|
providerSubscriptionId: string;
|
||||||
|
providerCustomerId?: string;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CheckoutParams = {
|
||||||
|
planCode: string;
|
||||||
|
complexId: string;
|
||||||
|
customerEmail: string;
|
||||||
|
successUrl: string;
|
||||||
|
cancelUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CheckoutResult = {
|
||||||
|
checkoutUrl: string;
|
||||||
|
providerSubscriptionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BillingProvider {
|
||||||
|
createCheckout(params: CheckoutParams): Promise<Result<CheckoutResult>>;
|
||||||
|
cancelSubscription(providerSubscriptionId: string): Promise<Result<void>>;
|
||||||
|
parseWebhook(
|
||||||
|
payload: unknown,
|
||||||
|
signature?: string,
|
||||||
|
requestId?: string
|
||||||
|
): Promise<Result<WebhookEvent>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { err, ok } from '@/lib/result';
|
||||||
|
import MercadoPagoConfig, { PreApproval } from 'mercadopago';
|
||||||
|
import { WebhookSignatureValidator } from 'mercadopago';
|
||||||
|
import type {
|
||||||
|
BillingProvider,
|
||||||
|
CheckoutParams,
|
||||||
|
CheckoutResult,
|
||||||
|
WebhookEvent,
|
||||||
|
} from './billing-provider.interface';
|
||||||
|
|
||||||
|
function getMpConfig(): MercadoPagoConfig {
|
||||||
|
const accessToken = Bun.env.MERCADOPAGO_ACCESS_TOKEN;
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('Missing MERCADOPAGO_ACCESS_TOKEN env var');
|
||||||
|
}
|
||||||
|
return new MercadoPagoConfig({ accessToken });
|
||||||
|
}
|
||||||
|
|
||||||
|
let _mpConfig: MercadoPagoConfig | null = null;
|
||||||
|
let _preApproval: PreApproval | null = null;
|
||||||
|
function preapproval(): PreApproval {
|
||||||
|
if (!_preApproval) {
|
||||||
|
_mpConfig = getMpConfig();
|
||||||
|
_preApproval = new PreApproval(_mpConfig);
|
||||||
|
}
|
||||||
|
return _preApproval;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMpPlanId(planCode: string, currency: string): string {
|
||||||
|
const key = `MERCADOPAGO_PLAN_ID_${currency}_${planCode}` as const;
|
||||||
|
const planId = Bun.env[key];
|
||||||
|
if (!planId) {
|
||||||
|
throw new Error(`Missing env var ${key} for Mercado Pago plan ID`);
|
||||||
|
}
|
||||||
|
return planId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSandbox(): boolean {
|
||||||
|
return Bun.env.BILLING_ENV !== 'production';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mercadopagoBillingProvider: BillingProvider = {
|
||||||
|
async createCheckout(params: CheckoutParams): Promise<Result<CheckoutResult>> {
|
||||||
|
try {
|
||||||
|
const { planCode, complexId, customerEmail, successUrl } = params;
|
||||||
|
|
||||||
|
const currency = 'ARS';
|
||||||
|
const planId = getMpPlanId(planCode, currency);
|
||||||
|
|
||||||
|
const response = await preapproval().create({
|
||||||
|
body: {
|
||||||
|
preapproval_plan_id: planId,
|
||||||
|
reason: `Suscripción ${planCode} - Playzer`,
|
||||||
|
payer_email: customerEmail,
|
||||||
|
external_reference: complexId,
|
||||||
|
back_url: successUrl,
|
||||||
|
status: isSandbox() ? 'pending' : undefined,
|
||||||
|
auto_recurring: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.id || !response.init_point) {
|
||||||
|
return err(Errors.unexpected('Mercado Pago did not return a preapproval ID or init_point'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok({
|
||||||
|
checkoutUrl: response.init_point,
|
||||||
|
providerSubscriptionId: response.id,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ error, params: { planCode: params.planCode } },
|
||||||
|
'MercadoPago createCheckout failed'
|
||||||
|
);
|
||||||
|
return err(Errors.unexpected('Failed to create Mercado Pago preapproval'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async cancelSubscription(providerSubscriptionId: string): Promise<Result<void>> {
|
||||||
|
try {
|
||||||
|
await preapproval().update({
|
||||||
|
id: providerSubscriptionId,
|
||||||
|
body: { status: 'cancelled' },
|
||||||
|
});
|
||||||
|
return ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error, providerSubscriptionId }, 'MercadoPago cancelSubscription failed');
|
||||||
|
return err(Errors.unexpected('Failed to cancel Mercado Pago preapproval'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async parseWebhook(
|
||||||
|
payload: unknown,
|
||||||
|
signature?: string,
|
||||||
|
requestId?: string
|
||||||
|
): Promise<Result<WebhookEvent>> {
|
||||||
|
try {
|
||||||
|
const body = payload as Record<string, unknown>;
|
||||||
|
|
||||||
|
const action = body?.action as string | undefined;
|
||||||
|
const dataId = (body?.data as Record<string, unknown>)?.id as string | undefined;
|
||||||
|
|
||||||
|
if (!action || !dataId) {
|
||||||
|
return err(
|
||||||
|
Errors.validation('Invalid Mercado Pago webhook payload: missing action or data.id')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signature && requestId) {
|
||||||
|
const secret = Bun.env.MERCADOPAGO_WEBHOOK_SECRET;
|
||||||
|
if (secret) {
|
||||||
|
try {
|
||||||
|
WebhookSignatureValidator.validate({
|
||||||
|
xSignature: signature,
|
||||||
|
xRequestId: requestId,
|
||||||
|
dataId,
|
||||||
|
secret,
|
||||||
|
toleranceSeconds: 300,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return err(Errors.validation('Invalid Mercado Pago webhook signature'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let webhookType: WebhookEvent['type'];
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'subscription_authorized_payment':
|
||||||
|
webhookType = 'activated';
|
||||||
|
break;
|
||||||
|
case 'subscription_cancelled':
|
||||||
|
webhookType = 'cancelled';
|
||||||
|
break;
|
||||||
|
case 'subscription_updated':
|
||||||
|
webhookType = 'updated';
|
||||||
|
break;
|
||||||
|
case 'subscription_charge_payment':
|
||||||
|
webhookType = 'payment_failed';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.info(
|
||||||
|
{ action },
|
||||||
|
'Mercado Pago webhook: unhandled action type, treating as updated'
|
||||||
|
);
|
||||||
|
webhookType = 'updated';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok({
|
||||||
|
providerEventId: `mp-${dataId}-${action}`,
|
||||||
|
type: webhookType,
|
||||||
|
providerSubscriptionId: dataId,
|
||||||
|
providerCustomerId: undefined,
|
||||||
|
data: body as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, 'MercadoPago webhook parsing failed');
|
||||||
|
return err(Errors.validation('Invalid Mercado Pago webhook payload'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
165
apps/backend/src/modules/billing/providers/stripe.provider.ts
Normal file
165
apps/backend/src/modules/billing/providers/stripe.provider.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { err, ok } from '@/lib/result';
|
||||||
|
import Stripe from 'stripe';
|
||||||
|
import type {
|
||||||
|
BillingProvider,
|
||||||
|
CheckoutParams,
|
||||||
|
CheckoutResult,
|
||||||
|
WebhookEvent,
|
||||||
|
} from './billing-provider.interface';
|
||||||
|
|
||||||
|
function getStripeClient(): Stripe {
|
||||||
|
const secretKey = Bun.env.STRIPE_SECRET_KEY;
|
||||||
|
if (!secretKey) {
|
||||||
|
throw new Error('Missing STRIPE_SECRET_KEY env var');
|
||||||
|
}
|
||||||
|
return new Stripe(secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _stripeClient: Stripe | null = null;
|
||||||
|
function stripe(): Stripe {
|
||||||
|
if (!_stripeClient) {
|
||||||
|
_stripeClient = getStripeClient();
|
||||||
|
}
|
||||||
|
return _stripeClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStripePriceId(planCode: string, currency: string): string {
|
||||||
|
const key = `STRIPE_PRICE_ID_${currency}_${planCode}` as const;
|
||||||
|
const priceId = Bun.env[key];
|
||||||
|
if (!priceId) {
|
||||||
|
throw new Error(`Missing env var ${key} for Stripe price ID`);
|
||||||
|
}
|
||||||
|
return priceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stripeBillingProvider: BillingProvider = {
|
||||||
|
async createCheckout(params: CheckoutParams): Promise<Result<CheckoutResult>> {
|
||||||
|
try {
|
||||||
|
const { planCode, complexId, customerEmail, successUrl, cancelUrl } = params;
|
||||||
|
|
||||||
|
const currency = 'USD';
|
||||||
|
|
||||||
|
const priceId = getStripePriceId(planCode, currency);
|
||||||
|
|
||||||
|
const session = await stripe().checkout.sessions.create({
|
||||||
|
mode: 'subscription',
|
||||||
|
customer_email: customerEmail,
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
price: priceId,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
complexId,
|
||||||
|
planCode,
|
||||||
|
},
|
||||||
|
subscription_data: {
|
||||||
|
metadata: {
|
||||||
|
complexId,
|
||||||
|
planCode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success_url: successUrl,
|
||||||
|
cancel_url: cancelUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session.url || !session.subscription) {
|
||||||
|
return err(Errors.unexpected('Stripe did not return a checkout URL or subscription ID'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionId =
|
||||||
|
typeof session.subscription === 'string' ? session.subscription : session.subscription.id;
|
||||||
|
|
||||||
|
return ok({
|
||||||
|
checkoutUrl: session.url,
|
||||||
|
providerSubscriptionId: subscriptionId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ error, params: { planCode: params.planCode } },
|
||||||
|
'Stripe createCheckout failed'
|
||||||
|
);
|
||||||
|
return err(Errors.unexpected('Failed to create Stripe checkout session'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async cancelSubscription(providerSubscriptionId: string): Promise<Result<void>> {
|
||||||
|
try {
|
||||||
|
await stripe().subscriptions.cancel(providerSubscriptionId);
|
||||||
|
return ok(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error, providerSubscriptionId }, 'Stripe cancelSubscription failed');
|
||||||
|
return err(Errors.unexpected('Failed to cancel Stripe subscription'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async parseWebhook(payload: unknown, signature?: string): Promise<Result<WebhookEvent>> {
|
||||||
|
if (!signature) {
|
||||||
|
return err(Errors.validation('Missing Stripe signature'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = Bun.env.STRIPE_WEBHOOK_SECRET;
|
||||||
|
if (!secret) {
|
||||||
|
return err(Errors.unexpected('Missing STRIPE_WEBHOOK_SECRET env var'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawBody = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
||||||
|
const event = stripe().webhooks.constructEvent(rawBody, signature, secret);
|
||||||
|
|
||||||
|
const eventType = event.type;
|
||||||
|
const data = event.data.object as unknown as Record<string, unknown>;
|
||||||
|
|
||||||
|
let webhookType: WebhookEvent['type'];
|
||||||
|
let providerSubscriptionId: string | undefined;
|
||||||
|
let providerCustomerId: string | undefined;
|
||||||
|
|
||||||
|
if (eventType === 'checkout.session.completed') {
|
||||||
|
webhookType = 'activated';
|
||||||
|
providerSubscriptionId = data.subscription as string | undefined;
|
||||||
|
providerCustomerId = data.customer as string | undefined;
|
||||||
|
} else if (eventType === 'customer.subscription.updated') {
|
||||||
|
const status = data.status as string | undefined;
|
||||||
|
if (status === 'past_due') {
|
||||||
|
webhookType = 'payment_failed';
|
||||||
|
} else if (status === 'active' || status === 'trialing') {
|
||||||
|
webhookType = 'updated';
|
||||||
|
} else if (status === 'canceled' || status === 'unpaid') {
|
||||||
|
webhookType = 'cancelled';
|
||||||
|
} else {
|
||||||
|
webhookType = 'updated';
|
||||||
|
}
|
||||||
|
providerSubscriptionId = data.id as string | undefined;
|
||||||
|
providerCustomerId = data.customer as string | undefined;
|
||||||
|
} else if (eventType === 'customer.subscription.deleted') {
|
||||||
|
webhookType = 'cancelled';
|
||||||
|
providerSubscriptionId = data.id as string | undefined;
|
||||||
|
providerCustomerId = data.customer as string | undefined;
|
||||||
|
} else if (eventType === 'invoice.payment_failed') {
|
||||||
|
webhookType = 'payment_failed';
|
||||||
|
providerSubscriptionId = data.subscription as string | undefined;
|
||||||
|
providerCustomerId = data.customer as string | undefined;
|
||||||
|
} else {
|
||||||
|
logger.info({ eventType }, 'Stripe webhook: unhandled event type, skipping');
|
||||||
|
webhookType = 'updated';
|
||||||
|
providerSubscriptionId =
|
||||||
|
(data as { subscription?: string })?.subscription ?? `unknown-${event.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok({
|
||||||
|
providerEventId: event.id,
|
||||||
|
type: webhookType,
|
||||||
|
providerSubscriptionId: providerSubscriptionId ?? `unknown-${event.id}`,
|
||||||
|
providerCustomerId,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, 'Stripe webhook parsing failed');
|
||||||
|
return err(Errors.validation('Invalid Stripe webhook signature or payload'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
|
||||||
|
export async function canUseApp(complexId: string): Promise<Result<true>> {
|
||||||
|
const billing = await db.complexBilling.findUnique({
|
||||||
|
where: { complexId },
|
||||||
|
select: { status: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!billing) {
|
||||||
|
return ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (billing.status === 'TRIAL' || billing.status === 'ACTIVE') {
|
||||||
|
return ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (billing.status === 'CANCELED') {
|
||||||
|
return err(
|
||||||
|
Errors.forbidden('Suscripción cancelada. Renueva tu plan para seguir usando la app.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (billing.status === 'PAST_DUE') {
|
||||||
|
return err(
|
||||||
|
Errors.forbidden(
|
||||||
|
'Suscripción con pago pendiente. Regularizá tu deuda para seguir usando la app.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (billing.status === 'SUSPENDED') {
|
||||||
|
return err(Errors.forbidden('Suscripción suspendida. Contactanos para reactivarla.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
|
export type BillingProviderEnum = 'STRIPE' | 'MERCADOPAGO' | 'PAYPAL';
|
||||||
|
export type BillingStatusEnum = 'TRIAL' | 'ACTIVE' | 'PAST_DUE' | 'CANCELED' | 'SUSPENDED';
|
||||||
|
|
||||||
|
export type ComplexBillingRow = {
|
||||||
|
complexId: string;
|
||||||
|
status: BillingStatusEnum;
|
||||||
|
planCode: string;
|
||||||
|
currency: string;
|
||||||
|
provider: BillingProviderEnum | null;
|
||||||
|
providerCustomerId: string | null;
|
||||||
|
providerSubscriptionId: string | null;
|
||||||
|
providerPreapprovalId: string | null;
|
||||||
|
currentPeriodStart: Date | null;
|
||||||
|
currentPeriodEnd: Date | null;
|
||||||
|
trialEndsAt: Date | null;
|
||||||
|
canceledAt: Date | null;
|
||||||
|
suspendedAt: Date | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getBilling(complexId: string): Promise<Result<ComplexBillingRow>> {
|
||||||
|
const billing = await db.complexBilling.findUnique({
|
||||||
|
where: { complexId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!billing) {
|
||||||
|
return err(Errors.notFound('Billing record not found for this complex'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(billing as unknown as ComplexBillingRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureBilling(
|
||||||
|
complexId: string,
|
||||||
|
currency: string
|
||||||
|
): Promise<Result<ComplexBillingRow>> {
|
||||||
|
const existing = await db.complexBilling.findUnique({
|
||||||
|
where: { complexId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return ok(existing as unknown as ComplexBillingRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await db.complexBilling.create({
|
||||||
|
data: {
|
||||||
|
complexId,
|
||||||
|
status: 'TRIAL',
|
||||||
|
planCode: 'BASIC',
|
||||||
|
currency,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok(created as unknown as ComplexBillingRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateBillingStatus(
|
||||||
|
complexId: string,
|
||||||
|
status: BillingStatusEnum,
|
||||||
|
extra?: {
|
||||||
|
provider?: BillingProviderEnum;
|
||||||
|
providerCustomerId?: string;
|
||||||
|
providerSubscriptionId?: string;
|
||||||
|
providerPreapprovalId?: string;
|
||||||
|
currentPeriodStart?: Date;
|
||||||
|
currentPeriodEnd?: Date;
|
||||||
|
}
|
||||||
|
): Promise<Result<ComplexBillingRow>> {
|
||||||
|
try {
|
||||||
|
const data: Record<string, unknown> = { status };
|
||||||
|
|
||||||
|
if (extra?.provider) data.provider = extra.provider;
|
||||||
|
if (extra?.providerCustomerId) data.providerCustomerId = extra.providerCustomerId;
|
||||||
|
if (extra?.providerSubscriptionId) data.providerSubscriptionId = extra.providerSubscriptionId;
|
||||||
|
if (extra?.providerPreapprovalId) data.providerPreapprovalId = extra.providerPreapprovalId;
|
||||||
|
if (extra?.currentPeriodStart) data.currentPeriodStart = extra.currentPeriodStart;
|
||||||
|
if (extra?.currentPeriodEnd) data.currentPeriodEnd = extra.currentPeriodEnd;
|
||||||
|
|
||||||
|
const updated = await db.complexBilling.update({
|
||||||
|
where: { complexId },
|
||||||
|
data: data as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok(updated as unknown as ComplexBillingRow);
|
||||||
|
} catch {
|
||||||
|
return err(Errors.unexpected('Failed to update billing status'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function recordBillingEvent(params: {
|
||||||
|
complexId: string;
|
||||||
|
eventType: string;
|
||||||
|
provider: BillingProviderEnum;
|
||||||
|
providerEventId: string | null;
|
||||||
|
providerData: unknown;
|
||||||
|
previousStatus: BillingStatusEnum | null;
|
||||||
|
newStatus: BillingStatusEnum | null;
|
||||||
|
}): Promise<Result<{ id: string }>> {
|
||||||
|
try {
|
||||||
|
const event = await db.billingEvent.create({
|
||||||
|
data: {
|
||||||
|
id: uuidv7(),
|
||||||
|
complexId: params.complexId,
|
||||||
|
eventType: params.eventType,
|
||||||
|
provider: params.provider,
|
||||||
|
providerEventId: params.providerEventId,
|
||||||
|
providerData: params.providerData as never,
|
||||||
|
previousStatus: params.previousStatus,
|
||||||
|
newStatus: params.newStatus,
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok({ id: event.id });
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const prismaError = error as { code?: string };
|
||||||
|
if (prismaError.code === 'P2002') {
|
||||||
|
return err(Errors.conflict('Webhook event already processed (idempotency)'));
|
||||||
|
}
|
||||||
|
return err(Errors.unexpected('Failed to record billing event'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findEventByProviderId(
|
||||||
|
providerEventId: string
|
||||||
|
): Promise<Result<{ id: string } | null>> {
|
||||||
|
const event = await db.billingEvent.findUnique({
|
||||||
|
where: { providerEventId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok(event ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasActiveSubscription(complexId: string): Promise<Result<boolean>> {
|
||||||
|
const billing = await db.complexBilling.findUnique({
|
||||||
|
where: { complexId },
|
||||||
|
select: { status: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!billing) return ok(false);
|
||||||
|
|
||||||
|
return ok(billing.status === 'ACTIVE' || billing.status === 'TRIAL');
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Errors } from '@/lib/errors';
|
||||||
|
import { db } from '@/lib/prisma';
|
||||||
|
import type { Result } from '@/lib/result';
|
||||||
|
import { err, ok } from '@/lib/result';
|
||||||
|
import type { BillingProvider } from '@/modules/billing/providers/billing-provider.interface';
|
||||||
|
import { mercadopagoBillingProvider } from '@/modules/billing/providers/mercadopago.provider';
|
||||||
|
import { stripeBillingProvider } from '@/modules/billing/providers/stripe.provider';
|
||||||
|
|
||||||
|
export const STRIPE = stripeBillingProvider;
|
||||||
|
export const MERCADOPAGO = mercadopagoBillingProvider;
|
||||||
|
|
||||||
|
export function resolveProviderByCountry(country: string | null): BillingProvider {
|
||||||
|
if (country === 'AR' || country?.toLowerCase() === 'argentina') {
|
||||||
|
return MERCADOPAGO;
|
||||||
|
}
|
||||||
|
return STRIPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProviderForComplex(
|
||||||
|
complexId: string
|
||||||
|
): Promise<Result<{ provider: BillingProvider; country: string | null }>> {
|
||||||
|
const complex = await db.complex.findUnique({
|
||||||
|
where: { id: complexId },
|
||||||
|
select: { country: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!complex) {
|
||||||
|
return err(Errors.notFound('Complex not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = resolveProviderByCountry(complex.country);
|
||||||
|
return ok({ provider, country: complex.country });
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { db } from '@/lib/prisma';
|
import { db } from '@/lib/prisma';
|
||||||
import { buildUniqueSlug, slugify } from '@/lib/slug';
|
import { buildUniqueSlug, slugify } from '@/lib/slug';
|
||||||
import { ensureActiveSport } from '@/modules/court/shared/guards';
|
|
||||||
import type { CreateComplexInput } from '@repo/api-contract';
|
import type { CreateComplexInput } from '@repo/api-contract';
|
||||||
import { v7 as uuidv7 } from 'uuid';
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
@@ -65,7 +64,15 @@ export async function createComplex(input: CreateComplexInternalInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input.setupCourts && input.courtSportId) {
|
if (input.setupCourts && input.courtSportId) {
|
||||||
const sport = await ensureActiveSport(input.courtSportId);
|
const sport = await tx.sport.findFirst({
|
||||||
|
where: { id: input.courtSportId, isActive: true },
|
||||||
|
select: { name: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sport) {
|
||||||
|
throw new Error('El deporte seleccionado no existe o esta inactivo.');
|
||||||
|
}
|
||||||
|
|
||||||
const availability = (input.courtDaysOfWeek ?? []).map((day) => ({
|
const availability = (input.courtDaysOfWeek ?? []).map((day) => ({
|
||||||
dayOfWeek: day as DayOfWeek,
|
dayOfWeek: day as DayOfWeek,
|
||||||
startTime: input.courtStartTime ?? '08:00',
|
startTime: input.courtStartTime ?? '08:00',
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ export type PriceableCourt = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ComplexWithPublicBookingData = Awaited<ReturnType<typeof getComplexWithBookingData>>;
|
export type ComplexWithPublicBookingData = {
|
||||||
|
courts: Array<{
|
||||||
|
sport: { id: string; name: string; slug: string; isActive: boolean };
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
export function toMinutes(value: string): number {
|
export function toMinutes(value: string): number {
|
||||||
const [hours, minutes] = value.split(':').map((part) => Number(part));
|
const [hours, minutes] = value.split(':').map((part) => Number(part));
|
||||||
@@ -169,9 +173,7 @@ export function validateSportSelection(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getComplexWithBookingData(
|
export async function getComplexWithBookingData(complexSlug: string) {
|
||||||
complexSlug: string
|
|
||||||
): Promise<Result<ComplexWithPublicBookingData>> {
|
|
||||||
const complex = await db.complex.findUnique({
|
const complex = await db.complex.findUnique({
|
||||||
where: { complexSlug },
|
where: { complexSlug },
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|||||||
import { adminBookingRoutes } from '@/modules/admin-booking/admin-booking.routes';
|
import { adminBookingRoutes } from '@/modules/admin-booking/admin-booking.routes';
|
||||||
import { adminRoutes } from '@/modules/admin/admin.routes';
|
import { adminRoutes } from '@/modules/admin/admin.routes';
|
||||||
import { authRoutes } from '@/modules/auth/auth.routes';
|
import { authRoutes } from '@/modules/auth/auth.routes';
|
||||||
|
import { billingRoutes } from '@/modules/billing/billing.routes';
|
||||||
import { complexRoutes } from '@/modules/complex/complex.routes';
|
import { complexRoutes } from '@/modules/complex/complex.routes';
|
||||||
import { courtRoutes } from '@/modules/court/court.routes';
|
import { courtRoutes } from '@/modules/court/court.routes';
|
||||||
import { passwordResetRoutes } from '@/modules/password-reset/password-reset.routes';
|
import { passwordResetRoutes } from '@/modules/password-reset/password-reset.routes';
|
||||||
@@ -27,6 +28,7 @@ export function registerRoutes(app: Hono<AppEnv>) {
|
|||||||
.route('/api/courts', courtRoutes)
|
.route('/api/courts', courtRoutes)
|
||||||
.route('/api/public-bookings', publicBookingRoutes)
|
.route('/api/public-bookings', publicBookingRoutes)
|
||||||
.route('/api/admin-bookings', adminBookingRoutes)
|
.route('/api/admin-bookings', adminBookingRoutes)
|
||||||
|
.route('/api/billing', billingRoutes)
|
||||||
.route('/api/health', healthCheckRoutes);
|
.route('/api/health', healthCheckRoutes);
|
||||||
|
|
||||||
registerEventsRoutes(app);
|
registerEventsRoutes(app);
|
||||||
|
|||||||
66
apps/backend/test/billing/duplicate-subscription.test.ts
Normal file
66
apps/backend/test/billing/duplicate-subscription.test.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, expect, it, mock } from 'bun:test';
|
||||||
|
import { prismaMock } from '../support/prisma.mock';
|
||||||
|
|
||||||
|
describe('duplicate-subscription', () => {
|
||||||
|
it('returns true when status is ACTIVE', async () => {
|
||||||
|
prismaMock.complexBilling.findUnique.mockResolvedValue({
|
||||||
|
status: 'ACTIVE',
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
const { hasActiveSubscription } = await import(
|
||||||
|
'@/modules/billing/services/billing-repository.service'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await hasActiveSubscription('complex-active');
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.value).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when status is TRIAL', async () => {
|
||||||
|
prismaMock.complexBilling.findUnique.mockResolvedValue({
|
||||||
|
status: 'TRIAL',
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
const { hasActiveSubscription } = await import(
|
||||||
|
'@/modules/billing/services/billing-repository.service'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await hasActiveSubscription('complex-trial');
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.value).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when status is CANCELED', async () => {
|
||||||
|
prismaMock.complexBilling.findUnique.mockResolvedValue({
|
||||||
|
status: 'CANCELED',
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
const { hasActiveSubscription } = await import(
|
||||||
|
'@/modules/billing/services/billing-repository.service'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await hasActiveSubscription('complex-canceled');
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.value).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when no billing record exists', async () => {
|
||||||
|
prismaMock.complexBilling.findUnique.mockResolvedValue(null as never);
|
||||||
|
|
||||||
|
const { hasActiveSubscription } = await import(
|
||||||
|
'@/modules/billing/services/billing-repository.service'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await hasActiveSubscription('complex-no-billing');
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.value).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
31
apps/backend/test/billing/event-mapping.test.ts
Normal file
31
apps/backend/test/billing/event-mapping.test.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { mapWebhookTypeToStatus } from './helpers';
|
||||||
|
|
||||||
|
describe('event-mapping', () => {
|
||||||
|
it('maps all webhook event types correctly', () => {
|
||||||
|
expect(mapWebhookTypeToStatus('activated')).toBe('ACTIVE');
|
||||||
|
expect(mapWebhookTypeToStatus('updated')).toBe('ACTIVE');
|
||||||
|
expect(mapWebhookTypeToStatus('payment_failed')).toBe('PAST_DUE');
|
||||||
|
expect(mapWebhookTypeToStatus('cancelled')).toBe('CANCELED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps Stripe checkout.session.completed -> activated -> ACTIVE', () => {
|
||||||
|
const status = mapWebhookTypeToStatus('activated');
|
||||||
|
expect(status).toBe('ACTIVE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps Stripe customer.subscription.deleted -> cancelled -> CANCELED', () => {
|
||||||
|
const status = mapWebhookTypeToStatus('cancelled');
|
||||||
|
expect(status).toBe('CANCELED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps Mercado Pago subscription_authorized_payment -> activated -> ACTIVE', () => {
|
||||||
|
const status = mapWebhookTypeToStatus('activated');
|
||||||
|
expect(status).toBe('ACTIVE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps Mercado Pago subscription_cancelled -> cancelled -> CANCELED', () => {
|
||||||
|
const status = mapWebhookTypeToStatus('cancelled');
|
||||||
|
expect(status).toBe('CANCELED');
|
||||||
|
});
|
||||||
|
});
|
||||||
22
apps/backend/test/billing/helpers.ts
Normal file
22
apps/backend/test/billing/helpers.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper that replicates the mapWebhookTypeToStatus logic
|
||||||
|
* used in webhook business files for testability.
|
||||||
|
*/
|
||||||
|
export function mapWebhookTypeToStatus(
|
||||||
|
eventType: 'activated' | 'updated' | 'payment_failed' | 'cancelled'
|
||||||
|
): 'ACTIVE' | 'PAST_DUE' | 'CANCELED' | null {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'activated':
|
||||||
|
return 'ACTIVE';
|
||||||
|
case 'updated':
|
||||||
|
return 'ACTIVE';
|
||||||
|
case 'payment_failed':
|
||||||
|
return 'PAST_DUE';
|
||||||
|
case 'cancelled':
|
||||||
|
return 'CANCELED';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
apps/backend/test/billing/idempotency.test.ts
Normal file
83
apps/backend/test/billing/idempotency.test.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { describe, expect, it, mock } from 'bun:test';
|
||||||
|
import { prismaMock } from '../support/prisma.mock';
|
||||||
|
|
||||||
|
describe('webhook-idempotency', () => {
|
||||||
|
it('returns conflict when providerEventId already exists', async () => {
|
||||||
|
const existingEvent = { id: 'event-1' };
|
||||||
|
|
||||||
|
prismaMock.billingEvent.findUnique.mockResolvedValue(existingEvent as never);
|
||||||
|
|
||||||
|
const { findEventByProviderId } = await import(
|
||||||
|
'@/modules/billing/services/billing-repository.service'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await findEventByProviderId('mp-duplicate-id');
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.value).toEqual({ id: 'event-1' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when providerEventId does not exist', async () => {
|
||||||
|
prismaMock.billingEvent.findUnique.mockResolvedValue(null as never);
|
||||||
|
|
||||||
|
const { findEventByProviderId } = await import(
|
||||||
|
'@/modules/billing/services/billing-repository.service'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await findEventByProviderId('new-event-id');
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.value).toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('records a billing event successfully', async () => {
|
||||||
|
const createdEvent = { id: 'new-uuid' };
|
||||||
|
prismaMock.billingEvent.create.mockResolvedValue(createdEvent as never);
|
||||||
|
|
||||||
|
const { recordBillingEvent } = await import(
|
||||||
|
'@/modules/billing/services/billing-repository.service'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await recordBillingEvent({
|
||||||
|
complexId: 'complex-1',
|
||||||
|
eventType: 'activated',
|
||||||
|
provider: 'STRIPE',
|
||||||
|
providerEventId: 'evt_unique',
|
||||||
|
providerData: { some: 'data' },
|
||||||
|
previousStatus: 'TRIAL',
|
||||||
|
newStatus: 'ACTIVE',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.value.id).toBe('new-uuid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns conflict when billing event create has P2002 (duplicate)', async () => {
|
||||||
|
const prismaError = new Error('Unique constraint') as Error & { code: string };
|
||||||
|
prismaError.code = 'P2002';
|
||||||
|
prismaMock.billingEvent.create.mockRejectedValue(prismaError as never);
|
||||||
|
|
||||||
|
const { recordBillingEvent } = await import(
|
||||||
|
'@/modules/billing/services/billing-repository.service'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await recordBillingEvent({
|
||||||
|
complexId: 'complex-1',
|
||||||
|
eventType: 'activated',
|
||||||
|
provider: 'STRIPE',
|
||||||
|
providerEventId: 'evt_duplicate',
|
||||||
|
providerData: null,
|
||||||
|
previousStatus: null,
|
||||||
|
newStatus: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error.type).toBe('conflict');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
36
apps/backend/test/billing/provider-resolver.test.ts
Normal file
36
apps/backend/test/billing/provider-resolver.test.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { mercadopagoBillingProvider } from '@/modules/billing/providers/mercadopago.provider';
|
||||||
|
import { stripeBillingProvider } from '@/modules/billing/providers/stripe.provider';
|
||||||
|
import { resolveProviderByCountry } from '@/modules/billing/services/provider-resolver.service';
|
||||||
|
|
||||||
|
describe('provider-resolver', () => {
|
||||||
|
it('returns MERCADOPAGO for country AR', () => {
|
||||||
|
const provider = resolveProviderByCountry('AR');
|
||||||
|
expect(provider).toBe(mercadopagoBillingProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns STRIPE for country BR', () => {
|
||||||
|
const provider = resolveProviderByCountry('BR');
|
||||||
|
expect(provider).toBe(stripeBillingProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns STRIPE for country US', () => {
|
||||||
|
const provider = resolveProviderByCountry('US');
|
||||||
|
expect(provider).toBe(stripeBillingProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns STRIPE for null country', () => {
|
||||||
|
const provider = resolveProviderByCountry(null);
|
||||||
|
expect(provider).toBe(stripeBillingProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns STRIPE for empty country', () => {
|
||||||
|
const provider = resolveProviderByCountry('');
|
||||||
|
expect(provider).toBe(stripeBillingProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns STRIPE for any other ISO code', () => {
|
||||||
|
const provider = resolveProviderByCountry('CL');
|
||||||
|
expect(provider).toBe(stripeBillingProvider);
|
||||||
|
});
|
||||||
|
});
|
||||||
20
apps/backend/test/billing/status-transitions.test.ts
Normal file
20
apps/backend/test/billing/status-transitions.test.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { mapWebhookTypeToStatus } from './helpers';
|
||||||
|
|
||||||
|
describe('status-transitions', () => {
|
||||||
|
it('activated maps to ACTIVE', () => {
|
||||||
|
expect(mapWebhookTypeToStatus('activated')).toBe('ACTIVE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updated maps to ACTIVE', () => {
|
||||||
|
expect(mapWebhookTypeToStatus('updated')).toBe('ACTIVE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('payment_failed maps to PAST_DUE', () => {
|
||||||
|
expect(mapWebhookTypeToStatus('payment_failed')).toBe('PAST_DUE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancelled maps to CANCELED', () => {
|
||||||
|
expect(mapWebhookTypeToStatus('cancelled')).toBe('CANCELED');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import { beforeEach, expect, mock, test } from 'bun:test';
|
import { beforeEach, expect, mock, test } from 'bun:test';
|
||||||
|
|
||||||
import { Errors } from '@/lib/errors';
|
import { Errors } from '@/lib/errors';
|
||||||
import { err, ok } from '@/lib/result';
|
import { type Result, err, ok } from '@/lib/result';
|
||||||
import type { InviteComplexUserResponse } from '@repo/api-contract';
|
import type { InviteComplexUserResponse } from '@repo/api-contract';
|
||||||
|
|
||||||
const inviteComplexUserMock = mock(async () => undefined as unknown as InviteComplexUserResponse);
|
type InviteComplexUserFn = (
|
||||||
|
userId: string,
|
||||||
|
complexId: string,
|
||||||
|
input: { email: string }
|
||||||
|
) => Promise<Result<InviteComplexUserResponse>>;
|
||||||
|
|
||||||
|
const inviteComplexUserMock = mock<InviteComplexUserFn>(async () => undefined as never);
|
||||||
|
|
||||||
const { createInviteComplexUserHandler } = await import(
|
const { createInviteComplexUserHandler } = await import(
|
||||||
'@/modules/complex/features/invite-complex-user/invite-complex-user.handler'
|
'@/modules/complex/features/invite-complex-user/invite-complex-user.handler'
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ const createSportMock = mock(async (_input: CreateSportInput) =>
|
|||||||
name: 'Tenis',
|
name: 'Tenis',
|
||||||
slug: 'tenis',
|
slug: 'tenis',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
createdAt: '2026-04-23T00:00:00.000Z',
|
createdAt: new Date('2026-04-23T00:00:00.000Z'),
|
||||||
updatedAt: '2026-04-23T00:00:00.000Z',
|
updatedAt: new Date('2026-04-23T00:00:00.000Z'),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ test('returns 201 with the created sport', async () => {
|
|||||||
name: 'Tenis',
|
name: 'Tenis',
|
||||||
slug: 'tenis',
|
slug: 'tenis',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
createdAt: '2026-04-23T00:00:00.000Z',
|
createdAt: new Date('2026-04-23T00:00:00.000Z'),
|
||||||
updatedAt: '2026-04-23T00:00:00.000Z',
|
updatedAt: new Date('2026-04-23T00:00:00.000Z'),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
createSportMock.mockResolvedValue(ok(createdSport));
|
createSportMock.mockResolvedValue(ok(createdSport));
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export const dbMock = {
|
|||||||
courtBookingLog: prismaMock.courtBookingLog,
|
courtBookingLog: prismaMock.courtBookingLog,
|
||||||
courtAvailability: prismaMock.courtAvailability,
|
courtAvailability: prismaMock.courtAvailability,
|
||||||
courtPriceRule: prismaMock.courtPriceRule,
|
courtPriceRule: prismaMock.courtPriceRule,
|
||||||
|
complexBilling: prismaMock.complexBilling,
|
||||||
|
billingEvent: prismaMock.billingEvent,
|
||||||
$transaction: transactionMock,
|
$transaction: transactionMock,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,10 @@
|
|||||||
"./src/*"
|
"./src/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"rootDir": "./src",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "hono/jsx"
|
"jsxImportSource": "hono/jsx"
|
||||||
}
|
},
|
||||||
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
BIN
apps/frontend/src/assets/playzer-logo-transparent.webp
Normal file
BIN
apps/frontend/src/assets/playzer-logo-transparent.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,4 +1,4 @@
|
|||||||
import PlayzerLogo from '@/assets/playzer-logo-transparent.svg';
|
import PlayzerLogo from '@/assets/playzer-logo-transparent.webp';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import {
|
import {
|
||||||
|
|||||||
10
bun.lock
10
bun.lock
@@ -26,12 +26,14 @@
|
|||||||
"dotenv": "^17.4.1",
|
"dotenv": "^17.4.1",
|
||||||
"hono": "4.12.10",
|
"hono": "4.12.10",
|
||||||
"mailtrap": "^4.5.1",
|
"mailtrap": "^4.5.1",
|
||||||
|
"mercadopago": "^3.1.0",
|
||||||
"nodemailer": "^8.0.5",
|
"nodemailer": "^8.0.5",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
"pino": "10.3.1",
|
"pino": "10.3.1",
|
||||||
"pino-pretty": "13.1.3",
|
"pino-pretty": "13.1.3",
|
||||||
"pino-std-serializers": "7.1.0",
|
"pino-std-serializers": "7.1.0",
|
||||||
"prisma": "^7",
|
"prisma": "^7",
|
||||||
|
"stripe": "^22.2.2",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -625,7 +627,7 @@
|
|||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
|
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
|
||||||
|
|
||||||
@@ -731,7 +733,7 @@
|
|||||||
|
|
||||||
"bun-mock-prisma": ["bun-mock-prisma@1.2.1", "", { "peerDependencies": { "bun-types": ">=1.0.0" } }, "sha512-1c6CByUMk9acH7R87lcVoJqPpUWoqHRu6VwipXzU94s03/FXotACOt2DMgGhfz2xw5218BVoPJiH0QQXUwgmWg=="],
|
"bun-mock-prisma": ["bun-mock-prisma@1.2.1", "", { "peerDependencies": { "bun-types": ">=1.0.0" } }, "sha512-1c6CByUMk9acH7R87lcVoJqPpUWoqHRu6VwipXzU94s03/FXotACOt2DMgGhfz2xw5218BVoPJiH0QQXUwgmWg=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||||
|
|
||||||
@@ -1197,6 +1199,8 @@
|
|||||||
|
|
||||||
"memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
|
"memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
|
||||||
|
|
||||||
|
"mercadopago": ["mercadopago@3.1.0", "", {}, "sha512-3VB8AFpE8eZgm67e22kVbR19k9JC/1j66voLSh7m9tPvkASuRKNH2Y7yb793rO+fIpMtvl+wBj27UY4jIItWYA=="],
|
||||||
|
|
||||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||||
|
|
||||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||||
@@ -1569,6 +1573,8 @@
|
|||||||
|
|
||||||
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
|
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
|
||||||
|
|
||||||
|
"stripe": ["stripe@22.2.2", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-lRXLiarjW/I2QVa6QuFfz1Ma7Dc2yBQ7vlYH6NEmp5htMJNQGfiWExmOv0McfqY5wMCGV8DLuCvsyVRIPJlUUQ=="],
|
||||||
|
|
||||||
"strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="],
|
"strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||||
|
|||||||
283
docs/billing.md
Normal file
283
docs/billing.md
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
# Billing / Subscriptions Module
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
modules/billing/
|
||||||
|
├── billing.routes.ts # Rutas Hono
|
||||||
|
├── features/
|
||||||
|
│ ├── create-checkout/ # POST /api/billing/checkout
|
||||||
|
│ │ ├── create-checkout.handler.ts
|
||||||
|
│ │ └── create-checkout.business.ts
|
||||||
|
│ ├── cancel-subscription/ # POST /api/billing/cancel
|
||||||
|
│ │ ├── cancel-subscription.handler.ts
|
||||||
|
│ │ └── cancel-subscription.business.ts
|
||||||
|
│ ├── get-billing-status/ # GET /api/billing/status
|
||||||
|
│ │ ├── get-billing-status.handler.ts
|
||||||
|
│ │ └── get-billing-status.business.ts
|
||||||
|
│ ├── stripe-webhook/ # POST /api/billing/webhooks/stripe
|
||||||
|
│ │ ├── stripe-webhook.handler.ts
|
||||||
|
│ │ └── stripe-webhook.business.ts
|
||||||
|
│ └── mercadopago-webhook/ # POST /api/billing/webhooks/mercadopago
|
||||||
|
│ ├── mercadopago-webhook.handler.ts
|
||||||
|
│ └── mercadopago-webhook.business.ts
|
||||||
|
├── providers/
|
||||||
|
│ ├── billing-provider.interface.ts # BillingProvider abstraction
|
||||||
|
│ ├── stripe.provider.ts # Stripe implementation
|
||||||
|
│ └── mercadopago.provider.ts # Mercado Pago implementation
|
||||||
|
└── services/
|
||||||
|
├── provider-resolver.service.ts # Resuelve Stripe/MP por país
|
||||||
|
├── billing-repository.service.ts # DB access (Prisma)
|
||||||
|
└── billing-access.service.ts # canUseApp() helper
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resolución de provider
|
||||||
|
|
||||||
|
| Country | Provider |
|
||||||
|
|---------|----------|
|
||||||
|
| `AR` | Mercado Pago |
|
||||||
|
| Otro / null | Stripe |
|
||||||
|
|
||||||
|
La resolución usa el campo `country` del `Complex` (ISO 3166-1 alpha-2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variables de entorno
|
||||||
|
|
||||||
|
```
|
||||||
|
# Billing
|
||||||
|
BILLING_ENV=sandbox|production
|
||||||
|
FRONTEND_BASE_URL=http://localhost:5173
|
||||||
|
|
||||||
|
# Stripe
|
||||||
|
STRIPE_SECRET_KEY=sk_test_xxx
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
||||||
|
STRIPE_PRICE_ID_USD_BASIC=price_basic_usd
|
||||||
|
STRIPE_PRICE_ID_USD_ADVANCED=price_advanced_usd
|
||||||
|
STRIPE_PRICE_ID_USD_ENTERPRISE=price_enterprise_usd
|
||||||
|
|
||||||
|
# Mercado Pago (Argentina)
|
||||||
|
MERCADOPAGO_ACCESS_TOKEN=TEST-xxx
|
||||||
|
MERCADOPAGO_WEBHOOK_SECRET=xxx
|
||||||
|
MERCADOPAGO_PLAN_ID_ARS_BASIC=2c938084...
|
||||||
|
MERCADOPAGO_PLAN_ID_ARS_ADVANCED=2c938084...
|
||||||
|
MERCADOPAGO_PLAN_ID_ARS_ENTERPRISE=2c938084...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Naming de plans
|
||||||
|
|
||||||
|
Los IDs de prices/plans en Stripe y Mercado Pago se nombran con el código del Plan interno (`BASIC`, `ADVANCED`, `ENTERPRISE`) y la moneda (`USD`, `ARS`):
|
||||||
|
|
||||||
|
```
|
||||||
|
STRIPE_PRICE_ID_{CURRENCY}_{PLAN_CODE}
|
||||||
|
MERCADOPAGO_PLAN_ID_{CURRENCY}_{PLAN_CODE}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Método | Ruta | Auth | Descripción |
|
||||||
|
|--------|------|------|-------------|
|
||||||
|
| POST | `/api/billing/checkout` | `requireAuth` + cookie | Inicia checkout de suscripción |
|
||||||
|
| POST | `/api/billing/cancel` | `requireAuth` + cookie | Cancela suscripción activa |
|
||||||
|
| GET | `/api/billing/status` | `requireAuth` + cookie | Estado actual del billing |
|
||||||
|
| POST | `/api/billing/webhooks/stripe` | none (firma) | Webhook de Stripe |
|
||||||
|
| POST | `/api/billing/webhooks/mercadopago` | none (firma) | Webhook de Mercado Pago |
|
||||||
|
|
||||||
|
El `complexId` se obtiene de la cookie `selected-complex-id`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estados internos
|
||||||
|
|
||||||
|
```
|
||||||
|
TRIAL ──(checkout completed)──► ACTIVE
|
||||||
|
TRIAL ──(user cancels)────────► CANCELED
|
||||||
|
ACTIVE ──(payment fails)──────► PAST_DUE
|
||||||
|
PAST_DUE ──(retry ok)─────────► ACTIVE
|
||||||
|
PAST_DUE ──(user cancels)─────► CANCELED
|
||||||
|
ACTIVE ──(user cancels)───────► CANCELED
|
||||||
|
CANCELED ──(grace ends)───────► SUSPENDED
|
||||||
|
```
|
||||||
|
|
||||||
|
| Estado | Significado |
|
||||||
|
|--------|-------------|
|
||||||
|
| `TRIAL` | Período de prueba, puede usar la app |
|
||||||
|
| `ACTIVE` | Suscripción activa, puede usar la app |
|
||||||
|
| `PAST_DUE` | Pago fallido, acceso restringido |
|
||||||
|
| `CANCELED` | Cancelado por usuario, acceso restringido |
|
||||||
|
| `SUSPENDED` | Suspendido, acceso restringido |
|
||||||
|
|
||||||
|
### Helper: `canUseApp(complexId)`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { canUseApp } from '@/modules/billing/services/billing-access.service';
|
||||||
|
|
||||||
|
const result = await canUseApp(complexId);
|
||||||
|
if (!result.ok) {
|
||||||
|
// Rechazar acceso con mensaje según el estado
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Retorna `ok(true)` si el estado es `TRIAL` o `ACTIVE`. Caso contrario retorna un `AppError` tipo `forbidden` con un mensaje específico según el estado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flujo de Checkout
|
||||||
|
|
||||||
|
1. **Frontend** → `POST /api/billing/checkout` con `{ planCode: "BASIC" }`
|
||||||
|
2. **Backend** verifica:
|
||||||
|
- Usuario tiene acceso al complex (cookie)
|
||||||
|
- No existe suscripción activa
|
||||||
|
3. **Backend** resuelve provider según `Complex.country`
|
||||||
|
4. **Provider** genera URL de checkout (Stripe Checkout Session / Mercado Pago Preapproval)
|
||||||
|
5. **Backend** guarda/actualiza `ComplexBilling` con status `TRIAL`
|
||||||
|
6. **Frontend** redirige al usuario a `checkoutUrl`
|
||||||
|
7. **Usuario** completa pago en el provider
|
||||||
|
8. **Provider** envía webhook → backend procesa y pasa a `ACTIVE`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flujo de Webhooks
|
||||||
|
|
||||||
|
### Idempotencia
|
||||||
|
|
||||||
|
Cada webhook tiene un `providerEventId` único (Stripe: `event.id`, MP: `mp-{data.id}-{action}`). El campo `providerEventId` en `BillingEvent` tiene `@unique`.
|
||||||
|
|
||||||
|
**Antes de procesar:**
|
||||||
|
1. Buscar `BillingEvent` por `providerEventId`
|
||||||
|
2. Si existe → responder 200 (ya procesado)
|
||||||
|
3. Si no existe → procesar y crear `BillingEvent` dentro de una transacción
|
||||||
|
|
||||||
|
### Stripe
|
||||||
|
|
||||||
|
```sh
|
||||||
|
stripe listen --forward-to localhost:3000/api/billing/webhooks/stripe
|
||||||
|
```
|
||||||
|
|
||||||
|
Stripe envía los eventos al endpoint con la firma en el header `stripe-signature`.
|
||||||
|
|
||||||
|
**Eventos manejados:**
|
||||||
|
|
||||||
|
| Evento | Acción |
|
||||||
|
|--------|--------|
|
||||||
|
| `checkout.session.completed` | Activar suscripción (`ACTIVE`) |
|
||||||
|
| `customer.subscription.updated` (active) | Actualizar período |
|
||||||
|
| `customer.subscription.updated` (past_due) | Marcar como `PAST_DUE` |
|
||||||
|
| `customer.subscription.deleted` | Cancelar (`CANCELED`) |
|
||||||
|
| `invoice.payment_failed` | Marcar como `PAST_DUE` |
|
||||||
|
|
||||||
|
### Mercado Pago
|
||||||
|
|
||||||
|
Usar ngrok para exponer el servidor local:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ngrok http 3000
|
||||||
|
# Configurar la URL del webhook en el dashboard de MP:
|
||||||
|
# https://www.mercadopago.com.ar/developers/panel/webhooks
|
||||||
|
# Apuntar a: https://{ngrok-id}.ngrok.app/api/billing/webhooks/mercadopago
|
||||||
|
```
|
||||||
|
|
||||||
|
**Eventos manejados:**
|
||||||
|
|
||||||
|
| Acción | Tipo |
|
||||||
|
|--------|------|
|
||||||
|
| `subscription_authorized_payment` | `activated` → `ACTIVE` |
|
||||||
|
| `subscription_cancelled` | `cancelled` → `CANCELED` |
|
||||||
|
| `subscription_updated` | `updated` → `ACTIVE` |
|
||||||
|
| `subscription_charge_payment` | `payment_failed` → `PAST_DUE` |
|
||||||
|
|
||||||
|
> **Nota:** Los nombres exactos de eventos de Mercado Pago pueden variar. Verificar en el dashboard de desarrolladores de MP durante integración.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modelo de datos (Prisma)
|
||||||
|
|
||||||
|
### `ComplexBilling`
|
||||||
|
|
||||||
|
| Columna | Tipo | Descripción |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `complexId` | `String @id` | FK a Complex |
|
||||||
|
| `status` | `BillingStatus` | `TRIAL`, `ACTIVE`, `PAST_DUE`, `CANCELED`, `SUSPENDED` |
|
||||||
|
| `planCode` | `String` | Código del Plan (BASIC, ADVANCED, ENTERPRISE) |
|
||||||
|
| `currency` | `String` | `ARS` o `USD` |
|
||||||
|
| `provider` | `BillingProvider?` | `STRIPE`, `MERCADOPAGO`, `PAYPAL` |
|
||||||
|
| `providerCustomerId` | `String?` | ID del customer en el provider |
|
||||||
|
| `providerSubscriptionId` | `String?` | ID de la suscripción en Stripe |
|
||||||
|
| `providerPreapprovalId` | `String?` | ID del preapproval en MP |
|
||||||
|
| `currentPeriodStart` | `DateTime?` | Inicio del período actual |
|
||||||
|
| `currentPeriodEnd` | `DateTime?` | Fin del período actual |
|
||||||
|
| `trialEndsAt` | `DateTime?` | Fin del trial |
|
||||||
|
| `canceledAt` | `DateTime?` | Fecha de cancelación |
|
||||||
|
| `suspendedAt` | `DateTime?` | Fecha de suspensión |
|
||||||
|
|
||||||
|
### `BillingEvent`
|
||||||
|
|
||||||
|
| Columna | Tipo | Descripción |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `id` | `String @id` | UUID |
|
||||||
|
| `complexId` | `String` | FK a ComplexBilling |
|
||||||
|
| `eventType` | `String` | Tipo de evento del provider |
|
||||||
|
| `provider` | `BillingProvider` | Provider que originó el evento |
|
||||||
|
| `providerEventId` | `String? @unique` | ID único del evento en el provider (idempotencia) |
|
||||||
|
| `providerData` | `Json?` | Payload completo del webhook |
|
||||||
|
| `previousStatus` | `BillingStatus?` | Estado anterior |
|
||||||
|
| `newStatus` | `BillingStatus?` | Nuevo estado |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agregar un nuevo provider (ej: PayPal)
|
||||||
|
|
||||||
|
1. Crear `providers/paypal.provider.ts` implementando `BillingProvider`
|
||||||
|
2. Agregar `PAYPAL` al enum `BillingProvider` en Prisma
|
||||||
|
3. Agregar case en `provider-resolver.service.ts` si aplica
|
||||||
|
4. Agregar webhook handler en `features/paypal-webhook/`
|
||||||
|
5. Agregar ruta en `billing.routes.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pruebas en local
|
||||||
|
|
||||||
|
### Sin cobrar dinero
|
||||||
|
|
||||||
|
- Stripe: Usar claves `sk_test_...`. Stripe automáticamente usa el entorno de prueba con tarjetas de prueba (`4242 4242 4242 4242`).
|
||||||
|
- Mercado Pago: Usar `BILLING_ENV=sandbox` con `MERCADOPAGO_ACCESS_TOKEN=TEST-...`.
|
||||||
|
|
||||||
|
### Stripe CLI (webhooks locales)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Instalar: https://stripe.com/docs/stripe-cli
|
||||||
|
stripe login
|
||||||
|
stripe listen --forward-to localhost:3000/api/billing/webhooks/stripe
|
||||||
|
# Copiar el webhook secret que muestra y ponerlo en STRIPE_WEBHOOK_SECRET
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mercado Pago + ngrok (webhooks locales)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ngrok http 3000
|
||||||
|
# Configurar webhook en: https://www.mercadopago.com.ar/developers/panel/webhooks
|
||||||
|
# URL: https://{ngrok-id}.ngrok.app/api/billing/webhooks/mercadopago
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comandos
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Instalar dependencias (ya instalado: stripe, mercadopago)
|
||||||
|
bun add stripe mercadopago
|
||||||
|
|
||||||
|
# Generar cliente Prisma
|
||||||
|
bun --filter backend prisma:generate
|
||||||
|
|
||||||
|
# Crear migración
|
||||||
|
bun --filter backend prisma:migrate
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
bun run test
|
||||||
|
|
||||||
|
# Dev server
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
34
packages/api-contract/src/billing.ts
Normal file
34
packages/api-contract/src/billing.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const billingProviderSchema = z.enum(['STRIPE', 'MERCADOPAGO', 'PAYPAL']);
|
||||||
|
export const billingStatusSchema = z.enum(['TRIAL', 'ACTIVE', 'PAST_DUE', 'CANCELED', 'SUSPENDED']);
|
||||||
|
|
||||||
|
export const createCheckoutSchema = z.object({
|
||||||
|
planCode: z.string().trim().min(1).max(10),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const checkoutResponseSchema = z.object({
|
||||||
|
checkoutUrl: z.string(),
|
||||||
|
subscriptionId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cancelSubscriptionSchema = z.object({});
|
||||||
|
|
||||||
|
export const billingStatusResponseSchema = z.object({
|
||||||
|
complexId: z.string(),
|
||||||
|
status: billingStatusSchema,
|
||||||
|
planCode: z.string(),
|
||||||
|
currency: z.string(),
|
||||||
|
provider: billingProviderSchema.nullable(),
|
||||||
|
currentPeriodEnd: z.string().nullable(),
|
||||||
|
currentPeriodStart: z.string().nullable(),
|
||||||
|
trialEndsAt: z.string().nullable(),
|
||||||
|
canceledAt: z.string().nullable(),
|
||||||
|
suspendedAt: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BillingProvider = z.infer<typeof billingProviderSchema>;
|
||||||
|
export type BillingStatus = z.infer<typeof billingStatusSchema>;
|
||||||
|
export type CreateCheckoutInput = z.infer<typeof createCheckoutSchema>;
|
||||||
|
export type CheckoutResponse = z.infer<typeof checkoutResponseSchema>;
|
||||||
|
export type BillingStatusResponse = z.infer<typeof billingStatusResponseSchema>;
|
||||||
@@ -155,6 +155,20 @@ export {
|
|||||||
adminGeoCountrySchema,
|
adminGeoCountrySchema,
|
||||||
adminGeoStatsSchema,
|
adminGeoStatsSchema,
|
||||||
} from './admin';
|
} from './admin';
|
||||||
|
export {
|
||||||
|
billingProviderSchema,
|
||||||
|
billingStatusSchema,
|
||||||
|
billingStatusResponseSchema,
|
||||||
|
createCheckoutSchema,
|
||||||
|
checkoutResponseSchema,
|
||||||
|
} from './billing';
|
||||||
|
export type {
|
||||||
|
BillingProvider,
|
||||||
|
BillingStatus,
|
||||||
|
BillingStatusResponse,
|
||||||
|
CreateCheckoutInput,
|
||||||
|
CheckoutResponse,
|
||||||
|
} from './billing';
|
||||||
export type {
|
export type {
|
||||||
AdminComplexListItem,
|
AdminComplexListItem,
|
||||||
AdminComplexStats,
|
AdminComplexStats,
|
||||||
|
|||||||
Reference in New Issue
Block a user