archetecture security fix
This commit is contained in:
Vendored
+1
@@ -0,0 +1 @@
|
||||
export * from './src/client'
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('./src/client')
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@rentaldrivego/database",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"db:generate": "prisma generate",
|
||||
"db:push": "prisma db push",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:migrate:deploy": "prisma migrate deploy",
|
||||
"db:seed": "ts-node --compiler-options '{\"module\":\"CommonJS\"}' prisma/seed.ts",
|
||||
"db:studio": "prisma studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^5.13.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.0",
|
||||
"@types/node": "^20.12.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"@types/bcryptjs": "^2.4.6"
|
||||
},
|
||||
"main": "./src/index.js",
|
||||
"types": "./src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.d.ts",
|
||||
"default": "./src/index.js"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./src/client.d.ts",
|
||||
"default": "./src/client.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+2
@@ -0,0 +1,2 @@
|
||||
-- Empty placeholder migration (no schema changes)
|
||||
SELECT 1;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "employees" ADD COLUMN IF NOT EXISTS "passwordHash" TEXT;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- AlterTable employees
|
||||
ALTER TABLE "employees" ADD COLUMN IF NOT EXISTS "passwordResetToken" TEXT UNIQUE;
|
||||
ALTER TABLE "employees" ADD COLUMN IF NOT EXISTS "passwordResetExpiresAt" TIMESTAMPTZ;
|
||||
|
||||
-- AlterTable admin_users
|
||||
ALTER TABLE "admin_users" ADD COLUMN IF NOT EXISTS "passwordResetToken" TEXT UNIQUE;
|
||||
ALTER TABLE "admin_users" ADD COLUMN IF NOT EXISTS "passwordResetExpiresAt" TIMESTAMPTZ;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CalendarBlockType" AS ENUM ('MANUAL', 'MAINTENANCE');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "vehicle_calendar_blocks" (
|
||||
"id" TEXT NOT NULL,
|
||||
"vehicleId" TEXT NOT NULL,
|
||||
"startDate" DATE NOT NULL,
|
||||
"endDate" DATE NOT NULL,
|
||||
"reason" TEXT,
|
||||
"type" "CalendarBlockType" NOT NULL DEFAULT 'MANUAL',
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "vehicle_calendar_blocks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "vehicle_calendar_blocks_vehicleId_idx" ON "vehicle_calendar_blocks"("vehicleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "vehicle_calendar_blocks_vehicleId_startDate_endDate_idx" ON "vehicle_calendar_blocks"("vehicleId", "startDate", "endDate");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "vehicle_calendar_blocks" ADD CONSTRAINT "vehicle_calendar_blocks_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Add reviewToken to reservations for secure one-time review links
|
||||
ALTER TABLE "reservations" ADD COLUMN "reviewToken" TEXT;
|
||||
CREATE UNIQUE INDEX "reservations_reviewToken_key" ON "reservations"("reviewToken");
|
||||
|
||||
-- Make renterId optional on reviews (reviews can come from non-marketplace customers via token link)
|
||||
ALTER TABLE "reviews" ALTER COLUMN "renterId" DROP NOT NULL;
|
||||
ALTER TABLE "reviews" DROP CONSTRAINT IF EXISTS "reviews_renterId_fkey";
|
||||
ALTER TABLE "reviews" ADD CONSTRAINT "reviews_renterId_fkey"
|
||||
FOREIGN KEY ("renterId") REFERENCES "renters"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "reservations" ADD COLUMN "vehicleCategory" "VehicleCategory";
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE "employees" ADD COLUMN "preferredLanguage" TEXT NOT NULL DEFAULT 'en';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "customers"
|
||||
ADD COLUMN "licenseImageUrl" TEXT;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "vehicles"
|
||||
ADD COLUMN "pickupLocations" TEXT[],
|
||||
ADD COLUMN "allowDifferentDropoff" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "dropoffLocations" TEXT[];
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
UPDATE "vehicles"
|
||||
SET
|
||||
"pickupLocations" = COALESCE("pickupLocations", ARRAY[]::TEXT[]),
|
||||
"dropoffLocations" = COALESCE("dropoffLocations", ARRAY[]::TEXT[]);
|
||||
|
||||
ALTER TABLE "vehicles"
|
||||
ALTER COLUMN "pickupLocations" SET DEFAULT ARRAY[]::TEXT[],
|
||||
ALTER COLUMN "pickupLocations" SET NOT NULL,
|
||||
ALTER COLUMN "dropoffLocations" SET DEFAULT ARRAY[]::TEXT[],
|
||||
ALTER COLUMN "dropoffLocations" SET NOT NULL;
|
||||
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE "pricing_configs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"plan" TEXT NOT NULL,
|
||||
"billingPeriod" TEXT NOT NULL,
|
||||
"amount" INTEGER NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"updatedBy" TEXT,
|
||||
|
||||
CONSTRAINT "pricing_configs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "pricing_configs_plan_billingPeriod_key" ON "pricing_configs"("plan", "billingPeriod");
|
||||
|
||||
-- Seed default prices (amounts in centimes MAD)
|
||||
INSERT INTO "pricing_configs" ("id", "plan", "billingPeriod", "amount", "updatedAt") VALUES
|
||||
('prc_starter_monthly', 'STARTER', 'MONTHLY', 2990, NOW()),
|
||||
('prc_starter_annual', 'STARTER', 'ANNUAL', 29900, NOW()),
|
||||
('prc_growth_monthly', 'GROWTH', 'MONTHLY', 3990, NOW()),
|
||||
('prc_growth_annual', 'GROWTH', 'ANNUAL', 39900, NOW()),
|
||||
('prc_pro_monthly', 'PRO', 'MONTHLY', 99900, NOW()),
|
||||
('prc_pro_annual', 'PRO', 'ANNUAL', 999000, NOW());
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
-- Add new SubscriptionStatus enum values
|
||||
ALTER TYPE "SubscriptionStatus" ADD VALUE IF NOT EXISTS 'PAYMENT_PENDING';
|
||||
ALTER TYPE "SubscriptionStatus" ADD VALUE IF NOT EXISTS 'SUSPENDED';
|
||||
ALTER TYPE "SubscriptionStatus" ADD VALUE IF NOT EXISTS 'EXPIRED';
|
||||
ALTER TYPE "SubscriptionStatus" ADD VALUE IF NOT EXISTS 'PAUSED';
|
||||
|
||||
-- Add new InvoiceStatus enum value
|
||||
ALTER TYPE "InvoiceStatus" ADD VALUE IF NOT EXISTS 'VOIDED';
|
||||
|
||||
-- Extend subscriptions table (camelCase to match Prisma default)
|
||||
ALTER TABLE "subscriptions"
|
||||
ADD COLUMN IF NOT EXISTS "trialUsed" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS "paymentPendingSince" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "paymentDueAt" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "pastDueSince" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "suspendedAt" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "endedAt" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "retryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS "maxRetryCount" INTEGER NOT NULL DEFAULT 5;
|
||||
|
||||
-- Extend subscription_invoices table
|
||||
ALTER TABLE "subscription_invoices"
|
||||
ADD COLUMN IF NOT EXISTS "providerInvoiceId" TEXT,
|
||||
ADD COLUMN IF NOT EXISTS "dueAt" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "failedAt" TIMESTAMP(3),
|
||||
ADD COLUMN IF NOT EXISTS "voidedAt" TIMESTAMP(3);
|
||||
|
||||
-- Create subscription_events table
|
||||
CREATE TABLE IF NOT EXISTS "subscription_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"subscriptionId" TEXT NOT NULL,
|
||||
"companyId" TEXT NOT NULL,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"source" TEXT NOT NULL DEFAULT 'system',
|
||||
"payload" JSONB NOT NULL DEFAULT '{}',
|
||||
"occurredAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "subscription_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "subscription_events_subscriptionId_idx" ON "subscription_events"("subscriptionId");
|
||||
CREATE INDEX IF NOT EXISTS "subscription_events_companyId_idx" ON "subscription_events"("companyId");
|
||||
|
||||
ALTER TABLE "subscription_events"
|
||||
ADD CONSTRAINT "subscription_events_subscriptionId_fkey"
|
||||
FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Create payment_attempts table
|
||||
CREATE TABLE IF NOT EXISTS "payment_attempts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"invoiceId" TEXT NOT NULL,
|
||||
"subscriptionId" TEXT NOT NULL,
|
||||
"providerPaymentId" TEXT,
|
||||
"status" TEXT NOT NULL,
|
||||
"failureCode" TEXT,
|
||||
"failureMessage" TEXT,
|
||||
"attemptedAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "payment_attempts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "payment_attempts_invoiceId_idx" ON "payment_attempts"("invoiceId");
|
||||
|
||||
ALTER TABLE "payment_attempts"
|
||||
ADD CONSTRAINT "payment_attempts_invoiceId_fkey"
|
||||
FOREIGN KEY ("invoiceId") REFERENCES "subscription_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS "pricing_promotions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"discountType" TEXT NOT NULL,
|
||||
"discountValue" INTEGER NOT NULL,
|
||||
"plans" TEXT[] NOT NULL DEFAULT '{}',
|
||||
"periods" TEXT[] NOT NULL DEFAULT '{}',
|
||||
"maxUses" INTEGER,
|
||||
"usedCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"validFrom" TIMESTAMP(3) NOT NULL,
|
||||
"validUntil" TIMESTAMP(3),
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdBy" TEXT,
|
||||
CONSTRAINT "pricing_promotions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "pricing_promotions_code_key" ON "pricing_promotions"("code");
|
||||
CREATE INDEX IF NOT EXISTS "pricing_promotions_isActive_idx" ON "pricing_promotions"("isActive");
|
||||
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE "plan_features" (
|
||||
"id" TEXT NOT NULL,
|
||||
"plan" "Plan" NOT NULL,
|
||||
"label" TEXT NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "plan_features_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "plan_features_plan_sortOrder_idx" ON "plan_features"("plan", "sortOrder");
|
||||
|
||||
INSERT INTO "plan_features" ("id", "plan", "label", "sortOrder") VALUES
|
||||
('plf_starter_1', 'STARTER', 'Up to 10 vehicles', 10),
|
||||
('plf_starter_2', 'STARTER', '1 user account', 20),
|
||||
('plf_starter_3', 'STARTER', 'Basic analytics', 30),
|
||||
('plf_starter_4', 'STARTER', 'Marketplace listing', 40),
|
||||
('plf_growth_1', 'GROWTH', 'Up to 50 vehicles', 10),
|
||||
('plf_growth_2', 'GROWTH', '5 user accounts', 20),
|
||||
('plf_growth_3', 'GROWTH', 'Full analytics', 30),
|
||||
('plf_growth_4', 'GROWTH', 'Priority marketplace placement', 40),
|
||||
('plf_growth_5', 'GROWTH', 'Custom branding', 50),
|
||||
('plf_pro_1', 'PRO', 'Unlimited vehicles', 10),
|
||||
('plf_pro_2', 'PRO', 'Unlimited user accounts', 20),
|
||||
('plf_pro_3', 'PRO', 'Advanced reports', 30),
|
||||
('plf_pro_4', 'PRO', 'API access', 40),
|
||||
('plf_pro_5', 'PRO', 'Dedicated support', 50);
|
||||
+493
@@ -0,0 +1,493 @@
|
||||
CREATE TYPE "BillingInvoiceTerms" AS ENUM (
|
||||
'DUE_ON_RECEIPT',
|
||||
'NET_7',
|
||||
'NET_15',
|
||||
'NET_30',
|
||||
'NET_45',
|
||||
'NET_60'
|
||||
);
|
||||
|
||||
CREATE TYPE "BillingInvoiceStatus" AS ENUM (
|
||||
'DRAFT',
|
||||
'OPEN',
|
||||
'PAID',
|
||||
'PARTIALLY_PAID',
|
||||
'PAYMENT_PENDING',
|
||||
'PAST_DUE',
|
||||
'VOID',
|
||||
'UNCOLLECTIBLE',
|
||||
'REFUNDED',
|
||||
'PARTIALLY_REFUNDED'
|
||||
);
|
||||
|
||||
CREATE TYPE "BillingInvoiceType" AS ENUM (
|
||||
'SUBSCRIPTION_INITIAL',
|
||||
'SUBSCRIPTION_RENEWAL',
|
||||
'TRIAL_CONVERSION',
|
||||
'SUBSCRIPTION_UPGRADE',
|
||||
'SUBSCRIPTION_DOWNGRADE_CREDIT',
|
||||
'USAGE_BASED',
|
||||
'ONE_TIME',
|
||||
'MANUAL',
|
||||
'CORRECTION',
|
||||
'CANCELLATION_FEE'
|
||||
);
|
||||
|
||||
CREATE TYPE "BillingLineItemType" AS ENUM (
|
||||
'SUBSCRIPTION_FEE',
|
||||
'SEAT_FEE',
|
||||
'USAGE_FEE',
|
||||
'SETUP_FEE',
|
||||
'DISCOUNT',
|
||||
'TAX',
|
||||
'CREDIT',
|
||||
'PRORATION',
|
||||
'REFUND_ADJUSTMENT',
|
||||
'MANUAL_ADJUSTMENT',
|
||||
'PROFESSIONAL_SERVICES',
|
||||
'CANCELLATION_FEE'
|
||||
);
|
||||
|
||||
CREATE TYPE "BillingPaymentMethodType" AS ENUM (
|
||||
'CARD',
|
||||
'ACH_DEBIT',
|
||||
'BANK_TRANSFER',
|
||||
'WIRE_TRANSFER',
|
||||
'MANUAL_INVOICE',
|
||||
'PURCHASE_ORDER'
|
||||
);
|
||||
|
||||
CREATE TYPE "BillingPaymentIntentStatus" AS ENUM (
|
||||
'REQUIRES_PAYMENT_METHOD',
|
||||
'REQUIRES_CONFIRMATION',
|
||||
'REQUIRES_ACTION',
|
||||
'PROCESSING',
|
||||
'SUCCEEDED',
|
||||
'FAILED',
|
||||
'CANCELED',
|
||||
'REFUNDED',
|
||||
'PARTIALLY_REFUNDED'
|
||||
);
|
||||
|
||||
CREATE TYPE "BillingPaymentAttemptStatus" AS ENUM (
|
||||
'PENDING',
|
||||
'PROCESSING',
|
||||
'SUCCEEDED',
|
||||
'FAILED',
|
||||
'CANCELED',
|
||||
'REFUNDED',
|
||||
'PARTIALLY_REFUNDED'
|
||||
);
|
||||
|
||||
CREATE TYPE "BillingRefundStatus" AS ENUM (
|
||||
'PENDING',
|
||||
'SUCCEEDED',
|
||||
'FAILED'
|
||||
);
|
||||
|
||||
CREATE TYPE "BillingCreditNoteStatus" AS ENUM (
|
||||
'ISSUED',
|
||||
'APPLIED',
|
||||
'VOID'
|
||||
);
|
||||
|
||||
CREATE SEQUENCE "billing_invoice_sequence_seq" START 1;
|
||||
|
||||
CREATE TABLE "billing_accounts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"companyId" TEXT NOT NULL,
|
||||
"isPrimary" BOOLEAN NOT NULL DEFAULT true,
|
||||
"legalName" TEXT NOT NULL,
|
||||
"billingEmail" TEXT NOT NULL,
|
||||
"billingAddress" JSONB,
|
||||
"taxId" TEXT,
|
||||
"taxExempt" BOOLEAN NOT NULL DEFAULT false,
|
||||
"defaultCurrency" TEXT NOT NULL DEFAULT 'MAD',
|
||||
"defaultPaymentMethodId" TEXT,
|
||||
"invoiceTerms" "BillingInvoiceTerms" NOT NULL DEFAULT 'DUE_ON_RECEIPT',
|
||||
"netTermsDays" INTEGER NOT NULL DEFAULT 0,
|
||||
"providerCustomerId" TEXT,
|
||||
"dunningPaused" BOOLEAN NOT NULL DEFAULT false,
|
||||
"dunningPausedAt" TIMESTAMP(3),
|
||||
"dunningPausedBy" TEXT,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_accounts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "billing_payment_methods" (
|
||||
"id" TEXT NOT NULL,
|
||||
"billingAccountId" TEXT NOT NULL,
|
||||
"type" "BillingPaymentMethodType" NOT NULL,
|
||||
"providerPaymentMethodId" TEXT,
|
||||
"label" TEXT,
|
||||
"brand" TEXT,
|
||||
"last4" TEXT,
|
||||
"expMonth" INTEGER,
|
||||
"expYear" INTEGER,
|
||||
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_payment_methods_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "billing_invoices" (
|
||||
"id" TEXT NOT NULL,
|
||||
"billingAccountId" TEXT NOT NULL,
|
||||
"companyId" TEXT NOT NULL,
|
||||
"subscriptionId" TEXT,
|
||||
"invoiceNumber" TEXT,
|
||||
"invoiceSequence" INTEGER,
|
||||
"invoiceType" "BillingInvoiceType" NOT NULL,
|
||||
"status" "BillingInvoiceStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"currency" TEXT NOT NULL DEFAULT 'MAD',
|
||||
"subtotalAmount" INTEGER NOT NULL DEFAULT 0,
|
||||
"discountAmount" INTEGER NOT NULL DEFAULT 0,
|
||||
"creditAmount" INTEGER NOT NULL DEFAULT 0,
|
||||
"taxAmount" INTEGER NOT NULL DEFAULT 0,
|
||||
"totalAmount" INTEGER NOT NULL DEFAULT 0,
|
||||
"amountPaid" INTEGER NOT NULL DEFAULT 0,
|
||||
"amountDue" INTEGER NOT NULL DEFAULT 0,
|
||||
"invoiceDate" TIMESTAMP(3),
|
||||
"dueAt" TIMESTAMP(3),
|
||||
"finalizedAt" TIMESTAMP(3),
|
||||
"paidAt" TIMESTAMP(3),
|
||||
"voidedAt" TIMESTAMP(3),
|
||||
"markedUncollectibleAt" TIMESTAMP(3),
|
||||
"billingName" TEXT,
|
||||
"billingEmail" TEXT,
|
||||
"billingAddress" JSONB,
|
||||
"providerInvoiceId" TEXT,
|
||||
"paymentProvider" "PaymentProvider",
|
||||
"isSubscriptionBlocking" BOOLEAN NOT NULL DEFAULT false,
|
||||
"adminReason" TEXT,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdByAdminId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_invoices_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "billing_invoice_line_items" (
|
||||
"id" TEXT NOT NULL,
|
||||
"invoiceId" TEXT NOT NULL,
|
||||
"subscriptionId" TEXT,
|
||||
"plan" "Plan",
|
||||
"type" "BillingLineItemType" NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"quantity" INTEGER NOT NULL DEFAULT 1,
|
||||
"unitAmount" INTEGER NOT NULL,
|
||||
"amount" INTEGER NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'MAD',
|
||||
"periodStart" TIMESTAMP(3),
|
||||
"periodEnd" TIMESTAMP(3),
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_invoice_line_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "billing_payment_intents" (
|
||||
"id" TEXT NOT NULL,
|
||||
"invoiceId" TEXT NOT NULL,
|
||||
"billingAccountId" TEXT NOT NULL,
|
||||
"paymentMethodId" TEXT,
|
||||
"providerPaymentIntentId" TEXT,
|
||||
"status" "BillingPaymentIntentStatus" NOT NULL DEFAULT 'REQUIRES_PAYMENT_METHOD',
|
||||
"amount" INTEGER NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'MAD',
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_payment_intents_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "billing_payment_attempts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"invoiceId" TEXT NOT NULL,
|
||||
"billingAccountId" TEXT NOT NULL,
|
||||
"paymentIntentId" TEXT,
|
||||
"paymentMethodId" TEXT,
|
||||
"providerPaymentId" TEXT,
|
||||
"status" "BillingPaymentAttemptStatus" NOT NULL,
|
||||
"amount" INTEGER NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'MAD',
|
||||
"failureCode" TEXT,
|
||||
"failureMessage" TEXT,
|
||||
"attemptedAt" TIMESTAMP(3) NOT NULL,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_payment_attempts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "billing_credit_balances" (
|
||||
"id" TEXT NOT NULL,
|
||||
"billingAccountId" TEXT NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'MAD',
|
||||
"balanceAmount" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_credit_balances_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "billing_credit_ledger_entries" (
|
||||
"id" TEXT NOT NULL,
|
||||
"billingAccountId" TEXT NOT NULL,
|
||||
"invoiceId" TEXT,
|
||||
"type" TEXT NOT NULL,
|
||||
"amount" INTEGER NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'MAD',
|
||||
"reason" TEXT NOT NULL,
|
||||
"createdBy" TEXT,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_credit_ledger_entries_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "billing_credit_notes" (
|
||||
"id" TEXT NOT NULL,
|
||||
"invoiceId" TEXT NOT NULL,
|
||||
"billingAccountId" TEXT NOT NULL,
|
||||
"amount" INTEGER NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'MAD',
|
||||
"reason" TEXT NOT NULL,
|
||||
"status" "BillingCreditNoteStatus" NOT NULL DEFAULT 'ISSUED',
|
||||
"createdBy" TEXT,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_credit_notes_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "billing_refunds" (
|
||||
"id" TEXT NOT NULL,
|
||||
"invoiceId" TEXT NOT NULL,
|
||||
"paymentAttemptId" TEXT,
|
||||
"billingAccountId" TEXT NOT NULL,
|
||||
"amount" INTEGER NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'MAD',
|
||||
"reason" TEXT NOT NULL,
|
||||
"status" "BillingRefundStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"providerRefundId" TEXT,
|
||||
"createdBy" TEXT,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_refunds_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "billing_tax_records" (
|
||||
"id" TEXT NOT NULL,
|
||||
"invoiceId" TEXT NOT NULL,
|
||||
"jurisdiction" TEXT,
|
||||
"taxRate" DOUBLE PRECISION,
|
||||
"taxAmount" INTEGER NOT NULL,
|
||||
"taxType" TEXT,
|
||||
"taxExempt" BOOLEAN NOT NULL DEFAULT false,
|
||||
"exemptionReason" TEXT,
|
||||
"providerTaxId" TEXT,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_tax_records_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "billing_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"billingAccountId" TEXT,
|
||||
"invoiceId" TEXT,
|
||||
"subscriptionId" TEXT,
|
||||
"companyId" TEXT,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"source" TEXT NOT NULL,
|
||||
"payload" JSONB NOT NULL DEFAULT '{}',
|
||||
"occurredAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "billing_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "subscription_invoices"
|
||||
ADD COLUMN "billingInvoiceId" TEXT;
|
||||
|
||||
CREATE UNIQUE INDEX "subscription_invoices_billingInvoiceId_key" ON "subscription_invoices"("billingInvoiceId");
|
||||
CREATE UNIQUE INDEX "billing_invoices_invoiceNumber_key" ON "billing_invoices"("invoiceNumber");
|
||||
CREATE UNIQUE INDEX "billing_invoices_invoiceSequence_key" ON "billing_invoices"("invoiceSequence");
|
||||
CREATE UNIQUE INDEX "billing_credit_balances_billingAccountId_currency_key" ON "billing_credit_balances"("billingAccountId", "currency");
|
||||
|
||||
CREATE INDEX "billing_accounts_companyId_idx" ON "billing_accounts"("companyId");
|
||||
CREATE INDEX "billing_accounts_companyId_isPrimary_idx" ON "billing_accounts"("companyId", "isPrimary");
|
||||
CREATE INDEX "billing_payment_methods_billingAccountId_idx" ON "billing_payment_methods"("billingAccountId");
|
||||
CREATE INDEX "billing_invoices_billingAccountId_idx" ON "billing_invoices"("billingAccountId");
|
||||
CREATE INDEX "billing_invoices_companyId_idx" ON "billing_invoices"("companyId");
|
||||
CREATE INDEX "billing_invoices_subscriptionId_idx" ON "billing_invoices"("subscriptionId");
|
||||
CREATE INDEX "billing_invoice_line_items_invoiceId_idx" ON "billing_invoice_line_items"("invoiceId");
|
||||
CREATE INDEX "billing_payment_intents_invoiceId_idx" ON "billing_payment_intents"("invoiceId");
|
||||
CREATE INDEX "billing_payment_intents_billingAccountId_idx" ON "billing_payment_intents"("billingAccountId");
|
||||
CREATE INDEX "billing_payment_attempts_invoiceId_idx" ON "billing_payment_attempts"("invoiceId");
|
||||
CREATE INDEX "billing_payment_attempts_billingAccountId_idx" ON "billing_payment_attempts"("billingAccountId");
|
||||
CREATE INDEX "billing_credit_ledger_entries_billingAccountId_idx" ON "billing_credit_ledger_entries"("billingAccountId");
|
||||
CREATE INDEX "billing_credit_notes_invoiceId_idx" ON "billing_credit_notes"("invoiceId");
|
||||
CREATE INDEX "billing_credit_notes_billingAccountId_idx" ON "billing_credit_notes"("billingAccountId");
|
||||
CREATE INDEX "billing_refunds_invoiceId_idx" ON "billing_refunds"("invoiceId");
|
||||
CREATE INDEX "billing_refunds_billingAccountId_idx" ON "billing_refunds"("billingAccountId");
|
||||
CREATE INDEX "billing_tax_records_invoiceId_idx" ON "billing_tax_records"("invoiceId");
|
||||
CREATE INDEX "billing_events_billingAccountId_idx" ON "billing_events"("billingAccountId");
|
||||
CREATE INDEX "billing_events_invoiceId_idx" ON "billing_events"("invoiceId");
|
||||
CREATE INDEX "billing_events_subscriptionId_idx" ON "billing_events"("subscriptionId");
|
||||
CREATE INDEX "billing_events_companyId_idx" ON "billing_events"("companyId");
|
||||
|
||||
ALTER TABLE "billing_accounts"
|
||||
ADD CONSTRAINT "billing_accounts_companyId_fkey"
|
||||
FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_payment_methods"
|
||||
ADD CONSTRAINT "billing_payment_methods_billingAccountId_fkey"
|
||||
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_accounts"
|
||||
ADD CONSTRAINT "billing_accounts_defaultPaymentMethodId_fkey"
|
||||
FOREIGN KEY ("defaultPaymentMethodId") REFERENCES "billing_payment_methods"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_invoices"
|
||||
ADD CONSTRAINT "billing_invoices_billingAccountId_fkey"
|
||||
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_invoices"
|
||||
ADD CONSTRAINT "billing_invoices_companyId_fkey"
|
||||
FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_invoices"
|
||||
ADD CONSTRAINT "billing_invoices_subscriptionId_fkey"
|
||||
FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "subscription_invoices"
|
||||
ADD CONSTRAINT "subscription_invoices_billingInvoiceId_fkey"
|
||||
FOREIGN KEY ("billingInvoiceId") REFERENCES "billing_invoices"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_invoice_line_items"
|
||||
ADD CONSTRAINT "billing_invoice_line_items_invoiceId_fkey"
|
||||
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_payment_intents"
|
||||
ADD CONSTRAINT "billing_payment_intents_invoiceId_fkey"
|
||||
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_payment_intents"
|
||||
ADD CONSTRAINT "billing_payment_intents_billingAccountId_fkey"
|
||||
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_payment_intents"
|
||||
ADD CONSTRAINT "billing_payment_intents_paymentMethodId_fkey"
|
||||
FOREIGN KEY ("paymentMethodId") REFERENCES "billing_payment_methods"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_payment_attempts"
|
||||
ADD CONSTRAINT "billing_payment_attempts_invoiceId_fkey"
|
||||
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_payment_attempts"
|
||||
ADD CONSTRAINT "billing_payment_attempts_billingAccountId_fkey"
|
||||
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_payment_attempts"
|
||||
ADD CONSTRAINT "billing_payment_attempts_paymentIntentId_fkey"
|
||||
FOREIGN KEY ("paymentIntentId") REFERENCES "billing_payment_intents"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_payment_attempts"
|
||||
ADD CONSTRAINT "billing_payment_attempts_paymentMethodId_fkey"
|
||||
FOREIGN KEY ("paymentMethodId") REFERENCES "billing_payment_methods"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_credit_balances"
|
||||
ADD CONSTRAINT "billing_credit_balances_billingAccountId_fkey"
|
||||
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_credit_ledger_entries"
|
||||
ADD CONSTRAINT "billing_credit_ledger_entries_billingAccountId_fkey"
|
||||
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_credit_ledger_entries"
|
||||
ADD CONSTRAINT "billing_credit_ledger_entries_invoiceId_fkey"
|
||||
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_credit_notes"
|
||||
ADD CONSTRAINT "billing_credit_notes_invoiceId_fkey"
|
||||
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_credit_notes"
|
||||
ADD CONSTRAINT "billing_credit_notes_billingAccountId_fkey"
|
||||
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_refunds"
|
||||
ADD CONSTRAINT "billing_refunds_invoiceId_fkey"
|
||||
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_refunds"
|
||||
ADD CONSTRAINT "billing_refunds_paymentAttemptId_fkey"
|
||||
FOREIGN KEY ("paymentAttemptId") REFERENCES "billing_payment_attempts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_refunds"
|
||||
ADD CONSTRAINT "billing_refunds_billingAccountId_fkey"
|
||||
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_tax_records"
|
||||
ADD CONSTRAINT "billing_tax_records_invoiceId_fkey"
|
||||
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_events"
|
||||
ADD CONSTRAINT "billing_events_billingAccountId_fkey"
|
||||
FOREIGN KEY ("billingAccountId") REFERENCES "billing_accounts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_events"
|
||||
ADD CONSTRAINT "billing_events_invoiceId_fkey"
|
||||
FOREIGN KEY ("invoiceId") REFERENCES "billing_invoices"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_events"
|
||||
ADD CONSTRAINT "billing_events_subscriptionId_fkey"
|
||||
FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "billing_events"
|
||||
ADD CONSTRAINT "billing_events_companyId_fkey"
|
||||
FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
INSERT INTO "billing_accounts" (
|
||||
"id",
|
||||
"companyId",
|
||||
"isPrimary",
|
||||
"legalName",
|
||||
"billingEmail",
|
||||
"billingAddress",
|
||||
"taxExempt",
|
||||
"defaultCurrency",
|
||||
"invoiceTerms",
|
||||
"netTermsDays",
|
||||
"metadata"
|
||||
)
|
||||
SELECT
|
||||
'ba_' || c."id",
|
||||
c."id",
|
||||
true,
|
||||
c."name",
|
||||
c."email",
|
||||
c."address",
|
||||
false,
|
||||
COALESCE(a."currency", 'MAD'),
|
||||
'DUE_ON_RECEIPT'::"BillingInvoiceTerms",
|
||||
0,
|
||||
'{}'::jsonb
|
||||
FROM "companies" c
|
||||
LEFT JOIN "accounting_settings" a ON a."companyId" = c."id"
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "billing_accounts" ba
|
||||
WHERE ba."companyId" = c."id" AND ba."isPrimary" = true
|
||||
);
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
ALTER TYPE "NotificationType" ADD VALUE IF NOT EXISTS 'ACCOUNT_CREATED';
|
||||
|
||||
ALTER TABLE "billing_accounts"
|
||||
ADD COLUMN "preferredLanguage" TEXT NOT NULL DEFAULT 'en';
|
||||
|
||||
ALTER TABLE "notifications"
|
||||
ADD COLUMN "templateKey" TEXT,
|
||||
ADD COLUMN "locale" TEXT NOT NULL DEFAULT 'en';
|
||||
|
||||
CREATE TABLE "notification_templates" (
|
||||
"id" TEXT NOT NULL,
|
||||
"templateKey" TEXT NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"channel" "NotificationChannel" NOT NULL,
|
||||
"locale" TEXT NOT NULL,
|
||||
"subject" TEXT,
|
||||
"body" TEXT NOT NULL,
|
||||
"requiredVariables" JSONB,
|
||||
"optionalVariables" JSONB,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "notification_templates_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "notification_templates_templateKey_channel_locale_version_key"
|
||||
ON "notification_templates"("templateKey", "channel", "locale", "version");
|
||||
|
||||
CREATE INDEX "notification_templates_templateKey_channel_locale_isActive_idx"
|
||||
ON "notification_templates"("templateKey", "channel", "locale", "isActive");
|
||||
|
||||
INSERT INTO "notification_templates" (
|
||||
"id",
|
||||
"templateKey",
|
||||
"category",
|
||||
"channel",
|
||||
"locale",
|
||||
"subject",
|
||||
"body",
|
||||
"requiredVariables",
|
||||
"optionalVariables"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'notif_tpl_account_created_email_en',
|
||||
'account.created',
|
||||
'account',
|
||||
'EMAIL',
|
||||
'en',
|
||||
'Your workspace is ready - RentalDriveGo',
|
||||
E'Hi {{firstName}},\n\nYour RentalDriveGo workspace for {{companyName}} has been created successfully.\nPlan: {{planName}} ({{billingPeriodLabel}})\nCurrency: {{currency}}\nPrimary payment provider: {{paymentProvider}}\nFree trial ends on {{trialEndDate}}.\n\nYour workspace is ready. Sign in with the email and password you chose during signup.\n\nRentalDriveGo',
|
||||
'["firstName","companyName","planName","billingPeriodLabel","currency","paymentProvider","trialEndDate"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_account_created_email_fr',
|
||||
'account.created',
|
||||
'account',
|
||||
'EMAIL',
|
||||
'fr',
|
||||
'Votre espace de travail est pret - RentalDriveGo',
|
||||
E'Bonjour {{firstName}},\n\nVotre espace de travail RentalDriveGo pour {{companyName}} a ete cree avec succes.\nForfait : {{planName}} ({{billingPeriodLabel}})\nDevise : {{currency}}\nFournisseur de paiement principal : {{paymentProvider}}\nLa periode d''essai gratuit se termine le {{trialEndDate}}.\n\nVotre espace de travail est pret. Connectez-vous avec l''e-mail et le mot de passe choisis lors de l''inscription.\n\nRentalDriveGo',
|
||||
'["firstName","companyName","planName","billingPeriodLabel","currency","paymentProvider","trialEndDate"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_account_created_email_ar',
|
||||
'account.created',
|
||||
'account',
|
||||
'EMAIL',
|
||||
'ar',
|
||||
'مساحة عملك جاهزة - RentalDriveGo',
|
||||
E'مرحباً {{firstName}}،\n\nتم إنشاء مساحة عمل RentalDriveGo الخاصة بـ {{companyName}} بنجاح.\nالخطة: {{planName}} ({{billingPeriodLabel}})\nالعملة: {{currency}}\nمزود الدفع الرئيسي: {{paymentProvider}}\nتنتهي الفترة التجريبية المجانية في {{trialEndDate}}.\n\nمساحة عملك جاهزة. سجّل الدخول باستخدام البريد الإلكتروني وكلمة المرور التي اخترتهما عند التسجيل.\n\nRentalDriveGo',
|
||||
'["firstName","companyName","planName","billingPeriodLabel","currency","paymentProvider","trialEndDate"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_account_created_in_app_en',
|
||||
'account.created',
|
||||
'account',
|
||||
'IN_APP',
|
||||
'en',
|
||||
'Workspace ready',
|
||||
E'Your RentalDriveGo workspace for {{companyName}} is ready. Your free trial ends on {{trialEndDate}}.',
|
||||
'["companyName","trialEndDate"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_account_created_in_app_fr',
|
||||
'account.created',
|
||||
'account',
|
||||
'IN_APP',
|
||||
'fr',
|
||||
'Espace pret',
|
||||
E'Votre espace de travail RentalDriveGo pour {{companyName}} est pret. Votre essai gratuit se termine le {{trialEndDate}}.',
|
||||
'["companyName","trialEndDate"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_account_created_in_app_ar',
|
||||
'account.created',
|
||||
'account',
|
||||
'IN_APP',
|
||||
'ar',
|
||||
'مساحة العمل جاهزة',
|
||||
E'مساحة عمل RentalDriveGo الخاصة بـ {{companyName}} جاهزة. تنتهي الفترة التجريبية المجانية في {{trialEndDate}}.',
|
||||
'["companyName","trialEndDate"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_booking_confirmed_email_en',
|
||||
'booking.confirmed',
|
||||
'reservation',
|
||||
'EMAIL',
|
||||
'en',
|
||||
'Booking confirmed - {{companyName}}',
|
||||
E'Hi {{firstName}},\n\nYour booking with {{companyName}} has been confirmed.\nPickup: {{startDate}}\nReturn: {{endDate}}\nVehicle: {{vehicleName}}\n\nThank you for choosing RentalDriveGo.',
|
||||
'["firstName","companyName","startDate","endDate","vehicleName"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_booking_confirmed_email_fr',
|
||||
'booking.confirmed',
|
||||
'reservation',
|
||||
'EMAIL',
|
||||
'fr',
|
||||
'Reservation confirmee - {{companyName}}',
|
||||
E'Bonjour {{firstName}},\n\nVotre reservation chez {{companyName}} a ete confirmee.\nPrise en charge : {{startDate}}\nRetour : {{endDate}}\nVehicule : {{vehicleName}}\n\nMerci d''avoir choisi RentalDriveGo.',
|
||||
'["firstName","companyName","startDate","endDate","vehicleName"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_booking_confirmed_email_ar',
|
||||
'booking.confirmed',
|
||||
'reservation',
|
||||
'EMAIL',
|
||||
'ar',
|
||||
'تم تأكيد الحجز - {{companyName}}',
|
||||
E'مرحباً {{firstName}}،\n\nتم تأكيد حجزك مع {{companyName}}.\nالاستلام: {{startDate}}\nالإرجاع: {{endDate}}\nالمركبة: {{vehicleName}}\n\nشكراً لاختيار RentalDriveGo.',
|
||||
'["firstName","companyName","startDate","endDate","vehicleName"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_booking_confirmed_in_app_en',
|
||||
'booking.confirmed',
|
||||
'reservation',
|
||||
'IN_APP',
|
||||
'en',
|
||||
'Booking confirmed',
|
||||
E'Your booking with {{companyName}} has been confirmed.',
|
||||
'["companyName"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_booking_confirmed_in_app_fr',
|
||||
'booking.confirmed',
|
||||
'reservation',
|
||||
'IN_APP',
|
||||
'fr',
|
||||
'Reservation confirmee',
|
||||
E'Votre reservation chez {{companyName}} a ete confirmee.',
|
||||
'["companyName"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_booking_confirmed_in_app_ar',
|
||||
'booking.confirmed',
|
||||
'reservation',
|
||||
'IN_APP',
|
||||
'ar',
|
||||
'تم تأكيد الحجز',
|
||||
E'تم تأكيد حجزك مع {{companyName}}.',
|
||||
'["companyName"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_subscription_trial_ending_in_app_en',
|
||||
'subscription.trial_ending',
|
||||
'subscription',
|
||||
'IN_APP',
|
||||
'en',
|
||||
'Your trial ends soon',
|
||||
E'Your free trial ends on {{trialEndDate}}. Add a payment method to keep access.',
|
||||
'["trialEndDate"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_subscription_trial_ending_in_app_fr',
|
||||
'subscription.trial_ending',
|
||||
'subscription',
|
||||
'IN_APP',
|
||||
'fr',
|
||||
'Votre essai se termine bientot',
|
||||
E'Votre essai gratuit se termine le {{trialEndDate}}. Ajoutez un moyen de paiement pour conserver l''acces.',
|
||||
'["trialEndDate"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
),
|
||||
(
|
||||
'notif_tpl_subscription_trial_ending_in_app_ar',
|
||||
'subscription.trial_ending',
|
||||
'subscription',
|
||||
'IN_APP',
|
||||
'ar',
|
||||
'ستنتهي الفترة التجريبية قريباً',
|
||||
E'تنتهي فترتك التجريبية المجانية في {{trialEndDate}}. أضف وسيلة دفع للحفاظ على الوصول.',
|
||||
'["trialEndDate"]'::jsonb,
|
||||
'[]'::jsonb
|
||||
);
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
UPDATE "notification_templates"
|
||||
SET
|
||||
"body" = E'Hi {{firstName}},\n\nYour booking with {{companyName}} has been confirmed.\nPickup details: {{startDate}}\nPickup address: {{pickupLocation}}\nReturn: {{endDate}}\nVehicle: {{vehicleName}}\n\nThank you for choosing RentalDriveGo.',
|
||||
"requiredVariables" = '["firstName","companyName","startDate","pickupLocation","endDate","vehicleName"]'::jsonb,
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
WHERE "templateKey" = 'booking.confirmed'
|
||||
AND "channel" = 'EMAIL'
|
||||
AND "locale" = 'en';
|
||||
|
||||
UPDATE "notification_templates"
|
||||
SET
|
||||
"body" = E'Bonjour {{firstName}},\n\nVotre reservation chez {{companyName}} a ete confirmee.\nDetails de prise en charge : {{startDate}}\nAdresse de prise en charge : {{pickupLocation}}\nRetour : {{endDate}}\nVehicule : {{vehicleName}}\n\nMerci d''avoir choisi RentalDriveGo.',
|
||||
"requiredVariables" = '["firstName","companyName","startDate","pickupLocation","endDate","vehicleName"]'::jsonb,
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
WHERE "templateKey" = 'booking.confirmed'
|
||||
AND "channel" = 'EMAIL'
|
||||
AND "locale" = 'fr';
|
||||
|
||||
UPDATE "notification_templates"
|
||||
SET
|
||||
"body" = E'مرحباً {{firstName}}،\n\nتم تأكيد حجزك مع {{companyName}}.\nتفاصيل الاستلام: {{startDate}}\nعنوان الاستلام: {{pickupLocation}}\nالإرجاع: {{endDate}}\nالمركبة: {{vehicleName}}\n\nشكراً لاختيار RentalDriveGo.',
|
||||
"requiredVariables" = '["firstName","companyName","startDate","pickupLocation","endDate","vehicleName"]'::jsonb,
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
WHERE "templateKey" = 'booking.confirmed'
|
||||
AND "channel" = 'EMAIL'
|
||||
AND "locale" = 'ar';
|
||||
@@ -0,0 +1,19 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ReservationPhotoType" AS ENUM ('PICKUP', 'DROPOFF');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "reservation_photos" (
|
||||
"id" TEXT NOT NULL,
|
||||
"reservationId" TEXT NOT NULL,
|
||||
"type" "ReservationPhotoType" NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "reservation_photos_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "reservation_photos_reservationId_idx" ON "reservation_photos"("reservationId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "reservation_photos" ADD CONSTRAINT "reservation_photos_reservationId_fkey" FOREIGN KEY ("reservationId") REFERENCES "reservations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- AlterEnum (PostgreSQL enum values must be added outside a transaction)
|
||||
ALTER TYPE "VehicleStatus" ADD VALUE 'RESERVED';
|
||||
ALTER TYPE "VehicleStatus" ADD VALUE 'READY';
|
||||
ALTER TYPE "VehicleStatus" ADD VALUE 'RETURNED';
|
||||
ALTER TYPE "VehicleStatus" ADD VALUE 'NEEDS_CLEANING';
|
||||
ALTER TYPE "VehicleStatus" ADD VALUE 'DAMAGE_REVIEW';
|
||||
@@ -0,0 +1,83 @@
|
||||
-- CreateEnum: FeedbackCategory
|
||||
CREATE TYPE "FeedbackCategory" AS ENUM (
|
||||
'BOOKING',
|
||||
'PICKUP',
|
||||
'VEHICLE_CLEANLINESS',
|
||||
'VEHICLE_CONDITION',
|
||||
'STAFF_SERVICE',
|
||||
'PRICING',
|
||||
'DEPOSIT',
|
||||
'INSURANCE',
|
||||
'DAMAGE_CLAIM',
|
||||
'RETURN_PROCESS',
|
||||
'COMMUNICATION',
|
||||
'ROADSIDE_ASSISTANCE',
|
||||
'BILLING',
|
||||
'OTHER'
|
||||
);
|
||||
|
||||
-- CreateEnum: ComplaintSeverity
|
||||
CREATE TYPE "ComplaintSeverity" AS ENUM ('LEVEL_1', 'LEVEL_2', 'LEVEL_3');
|
||||
|
||||
-- CreateEnum: ComplaintStatus
|
||||
CREATE TYPE "ComplaintStatus" AS ENUM ('OPEN', 'INVESTIGATING', 'RESOLVED', 'CLOSED');
|
||||
|
||||
-- AlterTable: reviews — add category column
|
||||
ALTER TABLE "reviews" ADD COLUMN "category" "FeedbackCategory";
|
||||
|
||||
-- AlterTable: reservations — add review tracking columns
|
||||
ALTER TABLE "reservations"
|
||||
ADD COLUMN "reviewRequestSentAt" TIMESTAMP(3),
|
||||
ADD COLUMN "reviewReminderSentAt" TIMESTAMP(3),
|
||||
ADD COLUMN "reviewFinalReminderSentAt" TIMESTAMP(3),
|
||||
ADD COLUMN "reviewPaused" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- AlterTable: customers — add reviewOptOut column
|
||||
ALTER TABLE "customers" ADD COLUMN "reviewOptOut" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- CreateTable: complaints
|
||||
CREATE TABLE "complaints" (
|
||||
"id" TEXT NOT NULL,
|
||||
"companyId" TEXT NOT NULL,
|
||||
"reservationId" TEXT,
|
||||
"reviewId" TEXT,
|
||||
"customerId" TEXT,
|
||||
"severity" "ComplaintSeverity" NOT NULL DEFAULT 'LEVEL_1',
|
||||
"status" "ComplaintStatus" NOT NULL DEFAULT 'OPEN',
|
||||
"category" "FeedbackCategory" NOT NULL,
|
||||
"subject" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"resolution" TEXT,
|
||||
"notes" TEXT,
|
||||
"assignedTo" TEXT,
|
||||
"resolvedAt" TIMESTAMP(3),
|
||||
"resolvedBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "complaints_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "complaints_companyId_idx" ON "complaints"("companyId");
|
||||
CREATE INDEX "complaints_reservationId_idx" ON "complaints"("reservationId");
|
||||
|
||||
-- AddForeignKey: complaints → companies
|
||||
ALTER TABLE "complaints"
|
||||
ADD CONSTRAINT "complaints_companyId_fkey"
|
||||
FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey: complaints → reservations
|
||||
ALTER TABLE "complaints"
|
||||
ADD CONSTRAINT "complaints_reservationId_fkey"
|
||||
FOREIGN KEY ("reservationId") REFERENCES "reservations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey: complaints → reviews
|
||||
ALTER TABLE "complaints"
|
||||
ADD CONSTRAINT "complaints_reviewId_fkey"
|
||||
FOREIGN KEY ("reviewId") REFERENCES "reviews"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey: complaints → customers
|
||||
ALTER TABLE "complaints"
|
||||
ADD CONSTRAINT "complaints_customerId_fkey"
|
||||
FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
CREATE TYPE "VehiclePricingMode" AS ENUM ('MANUAL', 'AUTOMATIC');
|
||||
|
||||
CREATE TYPE "VehiclePricingRuleType" AS ENUM (
|
||||
'DATE_RANGE',
|
||||
'SEASONAL',
|
||||
'HOLIDAY',
|
||||
'WEEKEND',
|
||||
'SPECIAL_EVENT',
|
||||
'MANUAL_OVERRIDE'
|
||||
);
|
||||
|
||||
CREATE TYPE "VehiclePriceChangeSource" AS ENUM (
|
||||
'CONFIG_UPDATE',
|
||||
'RULE_CREATED',
|
||||
'RULE_UPDATED',
|
||||
'RULE_DELETED',
|
||||
'MANUAL_OVERRIDE'
|
||||
);
|
||||
|
||||
CREATE TABLE "vehicle_pricing_configurations" (
|
||||
"id" TEXT NOT NULL,
|
||||
"vehicleId" TEXT NOT NULL,
|
||||
"pricingMode" "VehiclePricingMode" NOT NULL DEFAULT 'MANUAL',
|
||||
"baseDailyRate" INTEGER,
|
||||
"weeklyRate" INTEGER,
|
||||
"weekendRate" INTEGER,
|
||||
"holidayRate" INTEGER,
|
||||
"monthlyRate" INTEGER,
|
||||
"longTermDailyRate" INTEGER,
|
||||
"minimumDailyRate" INTEGER,
|
||||
"maximumDailyRate" INTEGER,
|
||||
"maxDailyPriceMovementPct" INTEGER NOT NULL DEFAULT 10,
|
||||
"automaticPricingEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"approvalRequired" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "vehicle_pricing_configurations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "vehicle_pricing_configurations_vehicleId_key"
|
||||
ON "vehicle_pricing_configurations"("vehicleId");
|
||||
|
||||
CREATE TABLE "vehicle_pricing_rules" (
|
||||
"id" TEXT NOT NULL,
|
||||
"configurationId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"ruleType" "VehiclePricingRuleType" NOT NULL DEFAULT 'DATE_RANGE',
|
||||
"startDate" TIMESTAMP(3) NOT NULL,
|
||||
"endDate" TIMESTAMP(3) NOT NULL,
|
||||
"dailyRate" INTEGER,
|
||||
"weeklyRate" INTEGER,
|
||||
"weekendRate" INTEGER,
|
||||
"monthlyRate" INTEGER,
|
||||
"minDailyRate" INTEGER,
|
||||
"maxDailyRate" INTEGER,
|
||||
"automaticAdjustmentPct" INTEGER,
|
||||
"priceAdjustment" INTEGER,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "vehicle_pricing_rules_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "vehicle_pricing_rules_configurationId_idx"
|
||||
ON "vehicle_pricing_rules"("configurationId");
|
||||
|
||||
CREATE INDEX "vehicle_pricing_rules_configurationId_startDate_endDate_idx"
|
||||
ON "vehicle_pricing_rules"("configurationId", "startDate", "endDate");
|
||||
|
||||
CREATE TABLE "vehicle_price_history" (
|
||||
"id" TEXT NOT NULL,
|
||||
"configurationId" TEXT NOT NULL,
|
||||
"vehicleId" TEXT NOT NULL,
|
||||
"source" "VehiclePriceChangeSource" NOT NULL DEFAULT 'CONFIG_UPDATE',
|
||||
"changedByEmployeeId" TEXT,
|
||||
"previousDailyRate" INTEGER,
|
||||
"nextDailyRate" INTEGER,
|
||||
"previousWeeklyRate" INTEGER,
|
||||
"nextWeeklyRate" INTEGER,
|
||||
"note" TEXT,
|
||||
"effectiveFrom" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "vehicle_price_history_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "vehicle_price_history_vehicleId_createdAt_idx"
|
||||
ON "vehicle_price_history"("vehicleId", "createdAt");
|
||||
|
||||
CREATE INDEX "vehicle_price_history_configurationId_createdAt_idx"
|
||||
ON "vehicle_price_history"("configurationId", "createdAt");
|
||||
|
||||
ALTER TABLE "vehicle_pricing_configurations"
|
||||
ADD CONSTRAINT "vehicle_pricing_configurations_vehicleId_fkey"
|
||||
FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "vehicle_pricing_rules"
|
||||
ADD CONSTRAINT "vehicle_pricing_rules_configurationId_fkey"
|
||||
FOREIGN KEY ("configurationId") REFERENCES "vehicle_pricing_configurations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "vehicle_price_history"
|
||||
ADD CONSTRAINT "vehicle_price_history_configurationId_fkey"
|
||||
FOREIGN KEY ("configurationId") REFERENCES "vehicle_pricing_configurations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "vehicle_price_history"
|
||||
ADD CONSTRAINT "vehicle_price_history_vehicleId_fkey"
|
||||
FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,129 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "MenuItemType" AS ENUM (
|
||||
'INTERNAL_PAGE',
|
||||
'EXTERNAL_LINK',
|
||||
'PARENT_MENU',
|
||||
'SECTION_LABEL',
|
||||
'DIVIDER'
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "menu_items" (
|
||||
"id" TEXT NOT NULL,
|
||||
"systemKey" TEXT,
|
||||
"label" TEXT NOT NULL,
|
||||
"itemType" "MenuItemType" NOT NULL DEFAULT 'INTERNAL_PAGE',
|
||||
"routeOrUrl" TEXT,
|
||||
"icon" TEXT,
|
||||
"parentId" TEXT,
|
||||
"displayOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"openInNewTab" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isRequired" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdBy" TEXT,
|
||||
"updatedBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "menu_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "subscription_menu_items" (
|
||||
"id" TEXT NOT NULL,
|
||||
"plan" "Plan" NOT NULL,
|
||||
"menuItemId" TEXT NOT NULL,
|
||||
"displayOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "subscription_menu_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "company_menu_items" (
|
||||
"id" TEXT NOT NULL,
|
||||
"companyId" TEXT NOT NULL,
|
||||
"menuItemId" TEXT NOT NULL,
|
||||
"displayOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "company_menu_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "menu_item_role_visibility" (
|
||||
"id" TEXT NOT NULL,
|
||||
"menuItemId" TEXT NOT NULL,
|
||||
"role" "EmployeeRole" NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "menu_item_role_visibility_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "menu_items_systemKey_key" ON "menu_items"("systemKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "menu_items_parentId_idx" ON "menu_items"("parentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "menu_items_isActive_displayOrder_idx" ON "menu_items"("isActive", "displayOrder");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "subscription_menu_items_plan_menuItemId_key" ON "subscription_menu_items"("plan", "menuItemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "subscription_menu_items_menuItemId_idx" ON "subscription_menu_items"("menuItemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "subscription_menu_items_plan_displayOrder_idx" ON "subscription_menu_items"("plan", "displayOrder");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "company_menu_items_companyId_menuItemId_key" ON "company_menu_items"("companyId", "menuItemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "company_menu_items_menuItemId_idx" ON "company_menu_items"("menuItemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "company_menu_items_companyId_displayOrder_idx" ON "company_menu_items"("companyId", "displayOrder");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "menu_item_role_visibility_menuItemId_role_key" ON "menu_item_role_visibility"("menuItemId", "role");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "menu_item_role_visibility_role_idx" ON "menu_item_role_visibility"("role");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "menu_items"
|
||||
ADD CONSTRAINT "menu_items_parentId_fkey"
|
||||
FOREIGN KEY ("parentId") REFERENCES "menu_items"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "subscription_menu_items"
|
||||
ADD CONSTRAINT "subscription_menu_items_menuItemId_fkey"
|
||||
FOREIGN KEY ("menuItemId") REFERENCES "menu_items"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "company_menu_items"
|
||||
ADD CONSTRAINT "company_menu_items_companyId_fkey"
|
||||
FOREIGN KEY ("companyId") REFERENCES "companies"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "company_menu_items"
|
||||
ADD CONSTRAINT "company_menu_items_menuItemId_fkey"
|
||||
FOREIGN KEY ("menuItemId") REFERENCES "menu_items"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "menu_item_role_visibility"
|
||||
ADD CONSTRAINT "menu_item_role_visibility_menuItemId_fkey"
|
||||
FOREIGN KEY ("menuItemId") REFERENCES "menu_items"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
WITH canonical_menu_items ("systemKey", "label", "itemType", "routeOrUrl", "icon", "displayOrder", "isRequired", "isActive") AS (
|
||||
VALUES
|
||||
('dashboard', 'Dashboard', 'INTERNAL_PAGE'::"MenuItemType", '/', 'LayoutDashboard', 10, true, true),
|
||||
('fleet', 'Fleet', 'INTERNAL_PAGE'::"MenuItemType", '/fleet', 'Car', 20, true, true),
|
||||
('reservations', 'Booking', 'INTERNAL_PAGE'::"MenuItemType", '/reservations', 'Calendar', 30, true, true),
|
||||
('customers', 'Customers', 'INTERNAL_PAGE'::"MenuItemType", '/customers', 'Users', 40, false, true),
|
||||
('subscription', 'Subscription', 'INTERNAL_PAGE'::"MenuItemType", '/subscription', 'CreditCard', 50, true, true),
|
||||
('contracts', 'Contracts', 'INTERNAL_PAGE'::"MenuItemType", '/contracts', 'FileText', 60, false, true),
|
||||
('notifications', 'Notifications', 'INTERNAL_PAGE'::"MenuItemType", '/notifications', 'Bell', 70, false, true),
|
||||
('settings', 'Settings', 'INTERNAL_PAGE'::"MenuItemType", '/settings', 'Settings', 80, false, true),
|
||||
('onlineReservations', 'Online Reservations', 'INTERNAL_PAGE'::"MenuItemType", '/online-reservations', 'Globe', 90, false, true),
|
||||
('team', 'Team', 'INTERNAL_PAGE'::"MenuItemType", '/team', 'UserPlus', 100, false, true),
|
||||
('offers', 'Offers', 'INTERNAL_PAGE'::"MenuItemType", '/offers', 'Tag', 110, false, true),
|
||||
('reports', 'Reports', 'INTERNAL_PAGE'::"MenuItemType", '/reports', 'BarChart2', 120, false, true),
|
||||
('billing', 'Customer Billing', 'INTERNAL_PAGE'::"MenuItemType", '/billing', 'CreditCard', 130, false, true),
|
||||
('reviews', 'Reviews', 'INTERNAL_PAGE'::"MenuItemType", '/reviews', 'Star', 140, false, true),
|
||||
('complaints', 'Complaints', 'INTERNAL_PAGE'::"MenuItemType", '/complaints', 'AlertTriangle', 150, false, true)
|
||||
)
|
||||
INSERT INTO "menu_items" (
|
||||
"id",
|
||||
"systemKey",
|
||||
"label",
|
||||
"itemType",
|
||||
"routeOrUrl",
|
||||
"icon",
|
||||
"displayOrder",
|
||||
"openInNewTab",
|
||||
"isRequired",
|
||||
"isActive",
|
||||
"createdBy",
|
||||
"updatedBy"
|
||||
)
|
||||
SELECT
|
||||
'menu_' || "systemKey",
|
||||
"systemKey",
|
||||
"label",
|
||||
"itemType",
|
||||
"routeOrUrl",
|
||||
"icon",
|
||||
"displayOrder",
|
||||
false,
|
||||
"isRequired",
|
||||
"isActive",
|
||||
'system',
|
||||
'system'
|
||||
FROM canonical_menu_items
|
||||
ON CONFLICT ("systemKey") DO UPDATE
|
||||
SET
|
||||
"label" = EXCLUDED."label",
|
||||
"itemType" = EXCLUDED."itemType",
|
||||
"routeOrUrl" = EXCLUDED."routeOrUrl",
|
||||
"icon" = EXCLUDED."icon",
|
||||
"displayOrder" = EXCLUDED."displayOrder",
|
||||
"isRequired" = EXCLUDED."isRequired",
|
||||
"isActive" = EXCLUDED."isActive",
|
||||
"updatedBy" = 'system',
|
||||
"updatedAt" = CURRENT_TIMESTAMP;
|
||||
|
||||
DELETE FROM "subscription_menu_items"
|
||||
WHERE "menuItemId" IN (
|
||||
SELECT "id"
|
||||
FROM "menu_items"
|
||||
WHERE "systemKey" IN (
|
||||
'dashboard',
|
||||
'fleet',
|
||||
'reservations',
|
||||
'customers',
|
||||
'subscription',
|
||||
'contracts',
|
||||
'notifications',
|
||||
'settings',
|
||||
'onlineReservations',
|
||||
'team',
|
||||
'offers',
|
||||
'reports',
|
||||
'billing',
|
||||
'reviews',
|
||||
'complaints'
|
||||
)
|
||||
);
|
||||
|
||||
WITH canonical_subscription_assignments ("systemKey", "plan", "displayOrder") AS (
|
||||
VALUES
|
||||
('dashboard', 'STARTER'::"Plan", 10),
|
||||
('fleet', 'STARTER'::"Plan", 20),
|
||||
('reservations', 'STARTER'::"Plan", 30),
|
||||
('customers', 'STARTER'::"Plan", 40),
|
||||
('subscription', 'STARTER'::"Plan", 50),
|
||||
('contracts', 'STARTER'::"Plan", 60),
|
||||
('notifications', 'STARTER'::"Plan", 70),
|
||||
('settings', 'STARTER'::"Plan", 80),
|
||||
|
||||
('dashboard', 'GROWTH'::"Plan", 10),
|
||||
('fleet', 'GROWTH'::"Plan", 20),
|
||||
('reservations', 'GROWTH'::"Plan", 30),
|
||||
('customers', 'GROWTH'::"Plan", 40),
|
||||
('subscription', 'GROWTH'::"Plan", 50),
|
||||
('contracts', 'GROWTH'::"Plan", 60),
|
||||
('notifications', 'GROWTH'::"Plan", 70),
|
||||
('settings', 'GROWTH'::"Plan", 80),
|
||||
('onlineReservations', 'GROWTH'::"Plan", 90),
|
||||
('team', 'GROWTH'::"Plan", 100),
|
||||
('offers', 'GROWTH'::"Plan", 110),
|
||||
('reports', 'GROWTH'::"Plan", 120),
|
||||
('billing', 'GROWTH'::"Plan", 130),
|
||||
('reviews', 'GROWTH'::"Plan", 140),
|
||||
('complaints', 'GROWTH'::"Plan", 150),
|
||||
|
||||
('dashboard', 'PRO'::"Plan", 10),
|
||||
('fleet', 'PRO'::"Plan", 20),
|
||||
('reservations', 'PRO'::"Plan", 30),
|
||||
('customers', 'PRO'::"Plan", 40),
|
||||
('subscription', 'PRO'::"Plan", 50),
|
||||
('contracts', 'PRO'::"Plan", 60),
|
||||
('notifications', 'PRO'::"Plan", 70),
|
||||
('settings', 'PRO'::"Plan", 80),
|
||||
('onlineReservations', 'PRO'::"Plan", 90),
|
||||
('team', 'PRO'::"Plan", 100),
|
||||
('offers', 'PRO'::"Plan", 110),
|
||||
('reports', 'PRO'::"Plan", 120),
|
||||
('billing', 'PRO'::"Plan", 130),
|
||||
('reviews', 'PRO'::"Plan", 140),
|
||||
('complaints', 'PRO'::"Plan", 150)
|
||||
)
|
||||
INSERT INTO "subscription_menu_items" (
|
||||
"id",
|
||||
"plan",
|
||||
"menuItemId",
|
||||
"displayOrder",
|
||||
"isActive"
|
||||
)
|
||||
SELECT
|
||||
'menu_sub_' || lower(csa."plan"::text) || '_' || csa."systemKey",
|
||||
csa."plan",
|
||||
mi."id",
|
||||
csa."displayOrder",
|
||||
true
|
||||
FROM canonical_subscription_assignments csa
|
||||
JOIN "menu_items" mi ON mi."systemKey" = csa."systemKey";
|
||||
|
||||
DELETE FROM "menu_item_role_visibility"
|
||||
WHERE "menuItemId" IN (
|
||||
SELECT "id"
|
||||
FROM "menu_items"
|
||||
WHERE "systemKey" IN (
|
||||
'dashboard',
|
||||
'fleet',
|
||||
'reservations',
|
||||
'customers',
|
||||
'subscription',
|
||||
'contracts',
|
||||
'notifications',
|
||||
'settings',
|
||||
'onlineReservations',
|
||||
'team',
|
||||
'offers',
|
||||
'reports',
|
||||
'billing',
|
||||
'reviews',
|
||||
'complaints'
|
||||
)
|
||||
);
|
||||
|
||||
WITH canonical_role_visibilities ("systemKey", "role") AS (
|
||||
VALUES
|
||||
('dashboard', 'OWNER'::"EmployeeRole"),
|
||||
('dashboard', 'MANAGER'::"EmployeeRole"),
|
||||
('dashboard', 'AGENT'::"EmployeeRole"),
|
||||
('fleet', 'OWNER'::"EmployeeRole"),
|
||||
('fleet', 'MANAGER'::"EmployeeRole"),
|
||||
('fleet', 'AGENT'::"EmployeeRole"),
|
||||
('reservations', 'OWNER'::"EmployeeRole"),
|
||||
('reservations', 'MANAGER'::"EmployeeRole"),
|
||||
('reservations', 'AGENT'::"EmployeeRole"),
|
||||
('customers', 'OWNER'::"EmployeeRole"),
|
||||
('customers', 'MANAGER'::"EmployeeRole"),
|
||||
('customers', 'AGENT'::"EmployeeRole"),
|
||||
('subscription', 'OWNER'::"EmployeeRole"),
|
||||
('contracts', 'OWNER'::"EmployeeRole"),
|
||||
('contracts', 'MANAGER'::"EmployeeRole"),
|
||||
('contracts', 'AGENT'::"EmployeeRole"),
|
||||
('notifications', 'OWNER'::"EmployeeRole"),
|
||||
('notifications', 'MANAGER'::"EmployeeRole"),
|
||||
('notifications', 'AGENT'::"EmployeeRole"),
|
||||
('settings', 'OWNER'::"EmployeeRole"),
|
||||
('onlineReservations', 'OWNER'::"EmployeeRole"),
|
||||
('onlineReservations', 'MANAGER'::"EmployeeRole"),
|
||||
('onlineReservations', 'AGENT'::"EmployeeRole"),
|
||||
('team', 'OWNER'::"EmployeeRole"),
|
||||
('team', 'MANAGER'::"EmployeeRole"),
|
||||
('team', 'AGENT'::"EmployeeRole"),
|
||||
('offers', 'OWNER'::"EmployeeRole"),
|
||||
('offers', 'MANAGER'::"EmployeeRole"),
|
||||
('reports', 'OWNER'::"EmployeeRole"),
|
||||
('reports', 'MANAGER'::"EmployeeRole"),
|
||||
('billing', 'OWNER'::"EmployeeRole"),
|
||||
('billing', 'MANAGER'::"EmployeeRole"),
|
||||
('reviews', 'OWNER'::"EmployeeRole"),
|
||||
('reviews', 'MANAGER'::"EmployeeRole"),
|
||||
('reviews', 'AGENT'::"EmployeeRole"),
|
||||
('complaints', 'OWNER'::"EmployeeRole"),
|
||||
('complaints', 'MANAGER'::"EmployeeRole"),
|
||||
('complaints', 'AGENT'::"EmployeeRole")
|
||||
)
|
||||
INSERT INTO "menu_item_role_visibility" (
|
||||
"id",
|
||||
"menuItemId",
|
||||
"role"
|
||||
)
|
||||
SELECT
|
||||
'menu_role_' || lower(crv."role"::text) || '_' || crv."systemKey",
|
||||
mi."id",
|
||||
crv."role"
|
||||
FROM canonical_role_visibilities crv
|
||||
JOIN "menu_items" mi ON mi."systemKey" = crv."systemKey";
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Company API keys are stored hash-only. Raw keys are shown once at creation.
|
||||
CREATE TABLE IF NOT EXISTS "company_api_keys" (
|
||||
"id" TEXT PRIMARY KEY,
|
||||
"companyId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"prefix" TEXT NOT NULL UNIQUE,
|
||||
"keyHash" TEXT NOT NULL,
|
||||
"lastUsedAt" TIMESTAMP(3),
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "company_api_keys_companyId_fkey"
|
||||
FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "company_api_keys_companyId_idx" ON "company_api_keys"("companyId");
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE IF NOT EXISTS "reservation_public_access" (
|
||||
"id" TEXT PRIMARY KEY,
|
||||
"reservationId" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL UNIQUE,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"usedAt" TIMESTAMP(3),
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "reservation_public_access_reservationId_fkey"
|
||||
FOREIGN KEY ("reservationId") REFERENCES "reservations"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "reservation_public_access_reservationId_idx" ON "reservation_public_access"("reservationId");
|
||||
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS "webhook_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"providerEventId" TEXT NOT NULL,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"payloadHash" TEXT NOT NULL,
|
||||
"errorMessage" TEXT,
|
||||
"receivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"processedAt" TIMESTAMP(3),
|
||||
CONSTRAINT "webhook_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "webhook_events_provider_providerEventId_key"
|
||||
ON "webhook_events"("provider", "providerEventId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "webhook_events_provider_status_idx"
|
||||
ON "webhook_events"("provider", "status");
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-- Remove legacy plaintext company API key storage.
|
||||
-- Company integrations must use company_api_keys, which stores only key hashes and prefixes.
|
||||
DROP INDEX IF EXISTS "companies_apiKey_key";
|
||||
ALTER TABLE "companies" DROP COLUMN IF EXISTS "apiKey";
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "admin_recovery_codes" (
|
||||
"id" TEXT NOT NULL,
|
||||
"adminUserId" TEXT NOT NULL,
|
||||
"codeHash" TEXT NOT NULL,
|
||||
"usedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "admin_recovery_codes_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "admin_recovery_codes_adminUserId_usedAt_idx" ON "admin_recovery_codes"("adminUserId", "usedAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "admin_recovery_codes" ADD CONSTRAINT "admin_recovery_codes_adminUserId_fkey" FOREIGN KEY ("adminUserId") REFERENCES "admin_users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1 @@
|
||||
provider = "postgresql"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
import { PrismaClient } from '../generated'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { ensureDatabaseUrl } from '../src/runtime-config'
|
||||
|
||||
ensureDatabaseUrl()
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
const email = process.env.ADMIN_SEED_EMAIL ?? 'admin@rentaldrivego.com'
|
||||
const password = process.env.ADMIN_SEED_PASSWORD ?? 'changeme123'
|
||||
const firstName = process.env.ADMIN_SEED_FIRST_NAME ?? 'Super'
|
||||
const lastName = process.env.ADMIN_SEED_LAST_NAME ?? 'Admin'
|
||||
|
||||
const existing = await prisma.adminUser.findUnique({ where: { email } })
|
||||
if (existing) {
|
||||
console.log(`Super admin already exists: ${email}`)
|
||||
return
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 12)
|
||||
|
||||
const admin = await prisma.adminUser.create({
|
||||
data: {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordHash,
|
||||
role: 'SUPER_ADMIN',
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`Created SUPER_ADMIN: ${admin.email} (id: ${admin.id})`)
|
||||
console.log('Login with the password you set in ADMIN_SEED_PASSWORD')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(() => prisma.$disconnect())
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { spawnSync } = require('node:child_process')
|
||||
const { readdirSync } = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const {
|
||||
ensureDatabaseUrl,
|
||||
loadDefaultEnvFiles,
|
||||
normalizeDatabaseUrlForExecution,
|
||||
} = require('../src/runtime-config')
|
||||
const { PrismaClient } = require('../generated')
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '../../..')
|
||||
const prismaBin = path.join(repoRoot, 'node_modules', '.bin', 'prisma')
|
||||
const schemaPath = path.join(repoRoot, 'packages/database/prisma/schema.prisma')
|
||||
const migrationsDir = path.join(repoRoot, 'packages/database/prisma/migrations')
|
||||
|
||||
function run(command, args) {
|
||||
const result = spawnSync(command, args, {
|
||||
stdio: 'inherit',
|
||||
cwd: repoRoot,
|
||||
env: process.env,
|
||||
})
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
loadDefaultEnvFiles(repoRoot)
|
||||
ensureDatabaseUrl()
|
||||
normalizeDatabaseUrlForExecution()
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
try {
|
||||
const [state] = await prisma.$queryRawUnsafe(`
|
||||
SELECT
|
||||
to_regclass('public.companies') IS NOT NULL AS "hasCompanies",
|
||||
to_regclass('public.employees') IS NOT NULL AS "hasEmployees",
|
||||
to_regclass('public._prisma_migrations') IS NOT NULL AS "hasMigrationsTable"
|
||||
`)
|
||||
|
||||
const failedMigrations = state.hasMigrationsTable
|
||||
? await prisma.$queryRawUnsafe(`
|
||||
SELECT migration_name
|
||||
FROM "_prisma_migrations"
|
||||
WHERE finished_at IS NULL
|
||||
AND rolled_back_at IS NULL
|
||||
ORDER BY started_at
|
||||
`)
|
||||
: []
|
||||
|
||||
const hasBaseSchema = state.hasCompanies || state.hasEmployees
|
||||
|
||||
if (!hasBaseSchema) {
|
||||
if (failedMigrations.length > 0) {
|
||||
console.log('[db:deploy] Fresh database with failed migration records detected; marking them rolled back before bootstrap')
|
||||
for (const migration of failedMigrations) {
|
||||
run(prismaBin, [
|
||||
'migrate',
|
||||
'resolve',
|
||||
'--schema',
|
||||
schemaPath,
|
||||
'--rolled-back',
|
||||
migration.migration_name,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[db:deploy] Fresh database detected; bootstrapping schema with prisma db push')
|
||||
run(prismaBin, ['db', 'push', '--schema', schemaPath])
|
||||
|
||||
const migrationNames = readdirSync(migrationsDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.sort()
|
||||
|
||||
if (migrationNames.length > 0) {
|
||||
console.log('[db:deploy] Marking existing migrations as applied for the bootstrapped schema')
|
||||
for (const migrationName of migrationNames) {
|
||||
run(prismaBin, [
|
||||
'migrate',
|
||||
'resolve',
|
||||
'--schema',
|
||||
schemaPath,
|
||||
'--applied',
|
||||
migrationName,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (failedMigrations.length > 0) {
|
||||
console.error('[db:deploy] Found failed migrations in a non-empty database. Resolve them manually before deploying again.')
|
||||
for (const migration of failedMigrations) {
|
||||
console.error(`- ${migration.migration_name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('[db:deploy] Existing database detected; applying Prisma migrations')
|
||||
run(prismaBin, ['migrate', 'deploy', '--schema', schemaPath])
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
if (error?.message?.includes("Can't reach database server at `localhost:5432`")) {
|
||||
console.error('[db:deploy] Hint: start the local Postgres service first. In this repo that is usually `docker compose -f docker-compose.dev.yml up -d postgres`.')
|
||||
}
|
||||
console.error('[db:deploy] Failed:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export { PrismaClient } from '../generated'
|
||||
export declare function ensureDatabaseUrl(env?: NodeJS.ProcessEnv): string | undefined
|
||||
@@ -0,0 +1,9 @@
|
||||
const { ensureDatabaseUrl } = require('./runtime-config')
|
||||
const { PrismaClient } = require('../generated')
|
||||
|
||||
ensureDatabaseUrl()
|
||||
|
||||
module.exports = {
|
||||
PrismaClient,
|
||||
ensureDatabaseUrl,
|
||||
}
|
||||
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
export declare function ensureDatabaseUrl(env?: NodeJS.ProcessEnv): string | undefined
|
||||
|
||||
export type AdminRole = 'SUPER_ADMIN' | 'ADMIN' | 'SUPPORT' | 'FINANCE' | 'VIEWER'
|
||||
export type EmployeeRole = 'OWNER' | 'MANAGER' | 'AGENT'
|
||||
export type LicenseStatus = 'PENDING' | 'VALID' | 'EXPIRING' | 'APPROVED' | 'DENIED' | 'EXPIRED'
|
||||
export type NotificationType =
|
||||
| 'ACCOUNT_CREATED'
|
||||
| 'NEW_BOOKING'
|
||||
| 'BOOKING_CANCELLED'
|
||||
| 'PAYMENT_RECEIVED'
|
||||
| 'PAYMENT_FAILED'
|
||||
| 'SUBSCRIPTION_TRIAL_ENDING'
|
||||
| 'SUBSCRIPTION_SUSPENDED'
|
||||
| 'VEHICLE_MAINTENANCE_DUE'
|
||||
| 'OFFER_EXPIRING'
|
||||
| 'NEW_REVIEW_RECEIVED'
|
||||
| 'BOOKING_CONFIRMED'
|
||||
| 'PICKUP_REMINDER_24H'
|
||||
| 'PICKUP_REMINDER_2H'
|
||||
| 'VEHICLE_READY'
|
||||
| 'RETURN_REMINDER'
|
||||
| 'BOOKING_CANCELLED_BY_COMPANY'
|
||||
| 'REFUND_PROCESSED'
|
||||
| 'NEW_OFFER_FROM_SAVED_COMPANY'
|
||||
| 'REVIEW_REQUEST'
|
||||
|
||||
export type NotificationChannel = 'EMAIL' | 'SMS' | 'WHATSAPP' | 'IN_APP' | 'PUSH'
|
||||
|
||||
export interface Company {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
email: string
|
||||
phone: string | null
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface Employee {
|
||||
id: string
|
||||
companyId: string
|
||||
clerkUserId: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
phone: string | null
|
||||
role: EmployeeRole
|
||||
isActive: boolean
|
||||
company?: Company
|
||||
}
|
||||
|
||||
export interface AdminUser {
|
||||
id: string
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
role: AdminRole
|
||||
isActive: boolean
|
||||
totpEnabled?: boolean
|
||||
totpSecret?: string | null
|
||||
}
|
||||
|
||||
export interface InsurancePolicy {
|
||||
id: string
|
||||
companyId: string
|
||||
name: string
|
||||
description?: string | null
|
||||
type: string
|
||||
chargeType: 'PER_DAY' | 'PER_RENTAL' | 'PERCENTAGE_OF_RENTAL'
|
||||
chargeValue: number
|
||||
isRequired: boolean
|
||||
isActive: boolean
|
||||
sortOrder: number
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const { ensureDatabaseUrl } = require('./runtime-config')
|
||||
|
||||
module.exports = {
|
||||
ensureDatabaseUrl,
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
export { ensureDatabaseUrl } from './runtime-config'
|
||||
|
||||
export type AdminRole = 'SUPER_ADMIN' | 'ADMIN' | 'SUPPORT' | 'FINANCE' | 'VIEWER'
|
||||
export type EmployeeRole = 'OWNER' | 'MANAGER' | 'AGENT'
|
||||
export type LicenseStatus = 'PENDING' | 'VALID' | 'EXPIRING' | 'APPROVED' | 'DENIED' | 'EXPIRED'
|
||||
export type NotificationType =
|
||||
| 'ACCOUNT_CREATED'
|
||||
| 'NEW_BOOKING'
|
||||
| 'BOOKING_CANCELLED'
|
||||
| 'PAYMENT_RECEIVED'
|
||||
| 'PAYMENT_FAILED'
|
||||
| 'SUBSCRIPTION_TRIAL_ENDING'
|
||||
| 'SUBSCRIPTION_SUSPENDED'
|
||||
| 'VEHICLE_MAINTENANCE_DUE'
|
||||
| 'OFFER_EXPIRING'
|
||||
| 'NEW_REVIEW_RECEIVED'
|
||||
| 'BOOKING_CONFIRMED'
|
||||
| 'PICKUP_REMINDER_24H'
|
||||
| 'PICKUP_REMINDER_2H'
|
||||
| 'VEHICLE_READY'
|
||||
| 'RETURN_REMINDER'
|
||||
| 'BOOKING_CANCELLED_BY_COMPANY'
|
||||
| 'REFUND_PROCESSED'
|
||||
| 'NEW_OFFER_FROM_SAVED_COMPANY'
|
||||
| 'REVIEW_REQUEST'
|
||||
|
||||
export type NotificationChannel = 'EMAIL' | 'SMS' | 'WHATSAPP' | 'IN_APP' | 'PUSH'
|
||||
|
||||
export interface Company {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
email: string
|
||||
phone: string | null
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface Employee {
|
||||
id: string
|
||||
companyId: string
|
||||
clerkUserId: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
phone: string | null
|
||||
role: EmployeeRole
|
||||
isActive: boolean
|
||||
company?: Company
|
||||
}
|
||||
|
||||
export interface AdminUser {
|
||||
id: string
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
role: AdminRole
|
||||
isActive: boolean
|
||||
totpEnabled?: boolean
|
||||
totpSecret?: string | null
|
||||
}
|
||||
|
||||
export interface InsurancePolicy {
|
||||
id: string
|
||||
companyId: string
|
||||
name: string
|
||||
description?: string | null
|
||||
type: string
|
||||
chargeType: 'PER_DAY' | 'PER_RENTAL' | 'PERCENTAGE_OF_RENTAL'
|
||||
chargeValue: number
|
||||
isRequired: boolean
|
||||
isActive: boolean
|
||||
sortOrder: number
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const ENABLED_VALUES = new Set(['1', 'true', 'yes'])
|
||||
|
||||
function parseEnvFile(content) {
|
||||
const env = {}
|
||||
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim()
|
||||
if (!line || line.startsWith('#')) continue
|
||||
|
||||
const normalized = line.startsWith('export ') ? line.slice(7).trim() : line
|
||||
const separatorIndex = normalized.indexOf('=')
|
||||
if (separatorIndex === -1) continue
|
||||
|
||||
const key = normalized.slice(0, separatorIndex).trim()
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue
|
||||
|
||||
let value = normalized.slice(separatorIndex + 1)
|
||||
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
const quote = value[0]
|
||||
value = value.slice(1, -1)
|
||||
|
||||
if (quote === '"') {
|
||||
value = value
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\r/g, '\r')
|
||||
.replace(/\\t/g, '\t')
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\\\/g, '\\')
|
||||
}
|
||||
}
|
||||
|
||||
env[key] = value
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
function loadEnvFile(filePath, env = process.env, { override = false } = {}) {
|
||||
if (!fs.existsSync(filePath)) return false
|
||||
const parsed = parseEnvFile(fs.readFileSync(filePath, 'utf8'))
|
||||
|
||||
for (const [key, value] of Object.entries(parsed)) {
|
||||
if (override || env[key] === undefined) {
|
||||
env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function isRunningInContainer() {
|
||||
return fs.existsSync('/.dockerenv')
|
||||
}
|
||||
|
||||
function normalizeDatabaseUrlForExecution(env = process.env) {
|
||||
if (!env.DATABASE_URL) return env.DATABASE_URL
|
||||
|
||||
let parsed
|
||||
try {
|
||||
parsed = new URL(env.DATABASE_URL)
|
||||
} catch {
|
||||
return env.DATABASE_URL
|
||||
}
|
||||
|
||||
if (parsed.hostname !== 'postgres' || isRunningInContainer()) {
|
||||
return env.DATABASE_URL
|
||||
}
|
||||
|
||||
const fallbackHost = env.POSTGRES_HOST_LOCAL ?? 'localhost'
|
||||
parsed.hostname = fallbackHost
|
||||
env.DATABASE_URL = parsed.toString()
|
||||
return env.DATABASE_URL
|
||||
}
|
||||
|
||||
function loadDefaultEnvFiles(baseDir, env = process.env) {
|
||||
const candidates = [
|
||||
path.join(baseDir, '.env'),
|
||||
path.join(baseDir, '.env.local'),
|
||||
path.join(baseDir, '.env.docker.dev'),
|
||||
]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
loadEnvFile(candidate, env)
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDatabaseUrl(env = process.env) {
|
||||
const shouldBuildFromPostgres = ENABLED_VALUES.has(
|
||||
env.DATABASE_URL_FROM_POSTGRES?.trim().toLowerCase() ?? '',
|
||||
)
|
||||
|
||||
if (!shouldBuildFromPostgres) {
|
||||
return env.DATABASE_URL
|
||||
}
|
||||
|
||||
if (!env.POSTGRES_PASSWORD) {
|
||||
throw new Error('DATABASE_URL_FROM_POSTGRES is enabled but POSTGRES_PASSWORD is not set')
|
||||
}
|
||||
|
||||
const user = env.POSTGRES_USER ?? 'postgres'
|
||||
const password = env.POSTGRES_PASSWORD
|
||||
const host = env.POSTGRES_HOST ?? 'postgres'
|
||||
const port = env.POSTGRES_PORT ?? '5432'
|
||||
const database = env.POSTGRES_DB ?? 'rentaldrivego'
|
||||
|
||||
env.DATABASE_URL = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${encodeURIComponent(database)}`
|
||||
|
||||
return env.DATABASE_URL
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureDatabaseUrl,
|
||||
loadDefaultEnvFiles,
|
||||
loadEnvFile,
|
||||
normalizeDatabaseUrlForExecution,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
const ENABLED_VALUES = new Set(['1', 'true', 'yes'])
|
||||
|
||||
export function ensureDatabaseUrl(env: NodeJS.ProcessEnv = process.env) {
|
||||
const shouldBuildFromPostgres = ENABLED_VALUES.has(
|
||||
env.DATABASE_URL_FROM_POSTGRES?.trim().toLowerCase() ?? '',
|
||||
)
|
||||
|
||||
if (!shouldBuildFromPostgres) {
|
||||
return env.DATABASE_URL
|
||||
}
|
||||
|
||||
if (!env.POSTGRES_PASSWORD) {
|
||||
throw new Error('DATABASE_URL_FROM_POSTGRES is enabled but POSTGRES_PASSWORD is not set')
|
||||
}
|
||||
|
||||
const user = env.POSTGRES_USER ?? 'postgres'
|
||||
const password = env.POSTGRES_PASSWORD
|
||||
const host = env.POSTGRES_HOST ?? 'postgres'
|
||||
const port = env.POSTGRES_PORT ?? '5432'
|
||||
const database = env.POSTGRES_DB ?? 'rentaldrivego'
|
||||
|
||||
env.DATABASE_URL = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${encodeURIComponent(database)}`
|
||||
|
||||
return env.DATABASE_URL
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["prisma/**/*"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
> @rentaldrivego/types@1.0.0 build
|
||||
> tsc
|
||||
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
export interface ApiSuccess<T> {
|
||||
data: T;
|
||||
}
|
||||
export interface ApiPaginated<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
export interface ApiError {
|
||||
error: string;
|
||||
message: string;
|
||||
statusCode: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export declare const PLAN_PRICES: Record<string, Record<string, Record<string, number>>>;
|
||||
export declare const PLAN_FEATURES: Record<string, string[]>;
|
||||
export type Locale = 'en' | 'fr' | 'ar';
|
||||
export type SupportedCurrency = 'MAD';
|
||||
export declare function getCurrencyLabel(locale?: Locale, _currency?: SupportedCurrency): string;
|
||||
export declare function formatCurrency(amount: number, _currency?: SupportedCurrency, locale?: Locale): string;
|
||||
Vendored
+71
@@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
// Standard API response shapes
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PLAN_FEATURES = exports.PLAN_PRICES = void 0;
|
||||
exports.getCurrencyLabel = getCurrencyLabel;
|
||||
exports.formatCurrency = formatCurrency;
|
||||
// Plan prices in smallest currency unit (MAD, in centimes)
|
||||
exports.PLAN_PRICES = {
|
||||
STARTER: {
|
||||
MONTHLY: { MAD: 2990 },
|
||||
ANNUAL: { MAD: 29900 },
|
||||
},
|
||||
GROWTH: {
|
||||
MONTHLY: { MAD: 3990 },
|
||||
ANNUAL: { MAD: 39900 },
|
||||
},
|
||||
PRO: {
|
||||
MONTHLY: { MAD: 99900 },
|
||||
ANNUAL: { MAD: 999000 },
|
||||
},
|
||||
};
|
||||
exports.PLAN_FEATURES = {
|
||||
STARTER: [
|
||||
'Up to 10 vehicles',
|
||||
'1 user account',
|
||||
'Basic analytics',
|
||||
'Marketplace listing',
|
||||
],
|
||||
GROWTH: [
|
||||
'Up to 50 vehicles',
|
||||
'5 user accounts',
|
||||
'Full analytics',
|
||||
'Priority marketplace placement',
|
||||
'Custom branding',
|
||||
],
|
||||
PRO: [
|
||||
'Unlimited vehicles',
|
||||
'Unlimited user accounts',
|
||||
'Advanced reports',
|
||||
'API access',
|
||||
'Dedicated support',
|
||||
],
|
||||
};
|
||||
function resolveLocale(locale) {
|
||||
if (locale)
|
||||
return locale;
|
||||
const lang = typeof globalThis === 'object' && 'document' in globalThis
|
||||
? globalThis.document?.documentElement?.lang
|
||||
: undefined;
|
||||
if (lang === 'en' || lang === 'fr' || lang === 'ar') {
|
||||
return lang;
|
||||
}
|
||||
return 'en';
|
||||
}
|
||||
function getCurrencyLabel(locale, _currency = 'MAD') {
|
||||
return resolveLocale(locale) === 'ar' ? 'درهم' : 'MAD';
|
||||
}
|
||||
function formatCurrency(amount, _currency = 'MAD', locale) {
|
||||
const resolvedLocale = resolveLocale(locale);
|
||||
const divisor = 100;
|
||||
const value = amount / divisor;
|
||||
const localeMap = { en: 'en-US', fr: 'fr-FR', ar: 'ar-MA' };
|
||||
const formattedValue = new Intl.NumberFormat(localeMap[resolvedLocale], {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
const currencyLabel = getCurrencyLabel(resolvedLocale, _currency);
|
||||
return resolvedLocale === 'en'
|
||||
? `${currencyLabel} ${formattedValue}`
|
||||
: `${formattedValue} ${currencyLabel}`;
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
export interface DamageZone {
|
||||
zone: VehicleZone;
|
||||
severity: 'SCRATCH' | 'DENT' | 'CRACK' | 'MISSING' | 'OTHER';
|
||||
note?: string;
|
||||
}
|
||||
export type VehicleZone = 'hood' | 'roof' | 'trunk' | 'front-left-door' | 'front-right-door' | 'rear-left-door' | 'rear-right-door' | 'front-left-fender' | 'front-right-fender' | 'rear-left-fender' | 'rear-right-fender' | 'front-bumper' | 'rear-bumper' | 'windshield' | 'rear-window' | 'left-mirror' | 'right-mirror' | 'front-left-wheel' | 'front-right-wheel' | 'rear-left-wheel' | 'rear-right-wheel' | 'interior' | 'under-hood' | 'undercarriage';
|
||||
export declare const DAMAGE_ZONE_COORDS: Record<VehicleZone, {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}>;
|
||||
export declare const SEVERITY_COLORS: Record<DamageZone['severity'], string>;
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.SEVERITY_COLORS = exports.DAMAGE_ZONE_COORDS = void 0;
|
||||
exports.DAMAGE_ZONE_COORDS = {
|
||||
'hood': { x: 48, y: 8, w: 64, h: 28 },
|
||||
'roof': { x: 48, y: 52, w: 64, h: 56 },
|
||||
'trunk': { x: 48, y: 124, w: 64, h: 28 },
|
||||
'front-bumper': { x: 48, y: 0, w: 64, h: 10 },
|
||||
'rear-bumper': { x: 48, y: 150, w: 64, h: 10 },
|
||||
'front-left-fender': { x: 20, y: 10, w: 30, h: 35 },
|
||||
'front-right-fender': { x: 110, y: 10, w: 30, h: 35 },
|
||||
'front-left-door': { x: 20, y: 52, w: 30, h: 30 },
|
||||
'front-right-door': { x: 110, y: 52, w: 30, h: 30 },
|
||||
'rear-left-door': { x: 20, y: 82, w: 30, h: 30 },
|
||||
'rear-right-door': { x: 110, y: 82, w: 30, h: 30 },
|
||||
'rear-left-fender': { x: 20, y: 112, w: 30, h: 35 },
|
||||
'rear-right-fender': { x: 110, y: 112, w: 30, h: 35 },
|
||||
'windshield': { x: 52, y: 38, w: 56, h: 18 },
|
||||
'rear-window': { x: 52, y: 104, w: 56, h: 18 },
|
||||
'left-mirror': { x: 8, y: 50, w: 14, h: 10 },
|
||||
'right-mirror': { x: 138, y: 50, w: 14, h: 10 },
|
||||
'front-left-wheel': { x: 10, y: 24, w: 22, h: 26 },
|
||||
'front-right-wheel': { x: 128, y: 24, w: 22, h: 26 },
|
||||
'rear-left-wheel': { x: 10, y: 110, w: 22, h: 26 },
|
||||
'rear-right-wheel': { x: 128, y: 110, w: 22, h: 26 },
|
||||
'interior': { x: 55, y: 58, w: 50, h: 44 },
|
||||
'under-hood': { x: 52, y: 10, w: 56, h: 20 },
|
||||
'undercarriage': { x: 52, y: 130, w: 56, h: 20 },
|
||||
};
|
||||
exports.SEVERITY_COLORS = {
|
||||
SCRATCH: '#F59E0B',
|
||||
DENT: '#EF4444',
|
||||
CRACK: '#8B5CF6',
|
||||
MISSING: '#374151',
|
||||
OTHER: '#6B7280',
|
||||
};
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export type FuelPolicyType = 'FULL_TO_FULL' | 'FULL_TO_EMPTY' | 'SAME_TO_SAME' | 'PREPAID' | 'FREE';
|
||||
export declare const FUEL_POLICY_LABELS: Record<FuelPolicyType, Record<'en' | 'fr' | 'ar', string>>;
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.FUEL_POLICY_LABELS = void 0;
|
||||
exports.FUEL_POLICY_LABELS = {
|
||||
FULL_TO_FULL: {
|
||||
en: 'Full-to-Full: Return vehicle with a full tank. A refueling fee applies if returned with less fuel.',
|
||||
fr: "Plein à plein : retournez le véhicule avec le plein. Des frais de ravitaillement s'appliquent sinon.",
|
||||
ar: 'ممتلئ إلى ممتلئ: يجب إعادة السيارة بخزان ممتلئ. رسوم إضافية في حال الإعادة بمستوى أقل.',
|
||||
},
|
||||
FULL_TO_EMPTY: {
|
||||
en: 'Full-to-Empty: Vehicle is provided with a full tank. No refund for unused fuel upon return.',
|
||||
fr: "Plein à vide : le véhicule est livré avec le plein. Aucun remboursement n'est accordé pour le carburant non utilisé.",
|
||||
ar: 'ممتلئ إلى فارغ: تُسلَّم السيارة بخزان ممتلئ. لا استرداد للوقود غير المستخدم.',
|
||||
},
|
||||
SAME_TO_SAME: {
|
||||
en: 'Same-to-Same: Return vehicle with the same fuel level as at pickup.',
|
||||
fr: "Même niveau : retournez le véhicule avec le même niveau de carburant qu'au départ.",
|
||||
ar: 'نفس المستوى: أعد السيارة بنفس مستوى الوقود عند الاستلام.',
|
||||
},
|
||||
PREPAID: {
|
||||
en: 'Prepaid Fuel: You pre-purchase a full tank at a fixed rate. Return at any fuel level.',
|
||||
fr: "Carburant prépayé : vous achetez un plein à tarif fixe à l'avance. Retour possible avec n'importe quel niveau.",
|
||||
ar: 'الوقود المدفوع مسبقاً: تدفع مقدماً ثمن خزان كامل بسعر ثابت. يمكن إعادة السيارة بأي مستوى.',
|
||||
},
|
||||
FREE: {
|
||||
en: 'Fuel Included: Fuel cost is included in the rental price.',
|
||||
fr: 'Carburant inclus : le coût du carburant est inclus dans le tarif de location.',
|
||||
ar: 'الوقود مشمول: تكلفة الوقود مشمولة في سعر الإيجار.',
|
||||
},
|
||||
};
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export * from './damage';
|
||||
export * from './fuel';
|
||||
export * from './api';
|
||||
export * from './marketplace-homepage';
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
__exportStar(require("./damage"), exports);
|
||||
__exportStar(require("./fuel"), exports);
|
||||
__exportStar(require("./api"), exports);
|
||||
__exportStar(require("./marketplace-homepage"), exports);
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
export type MarketplaceLanguage = 'en' | 'fr' | 'ar';
|
||||
export type MarketplaceHomepageMetric = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
export type MarketplaceHomepagePillar = {
|
||||
title: string;
|
||||
body: string;
|
||||
};
|
||||
export type MarketplaceHomepageStep = {
|
||||
step: string;
|
||||
title: string;
|
||||
body: string;
|
||||
};
|
||||
export type MarketplaceHomepageSectionType = 'hero' | 'surface' | 'pillars' | 'audiences' | 'features' | 'steps' | 'closing';
|
||||
export type MarketplaceHomepageContent = {
|
||||
sections: MarketplaceHomepageSectionType[];
|
||||
heroKicker: string;
|
||||
heroTitle: string;
|
||||
heroBody: string;
|
||||
startTrial: string;
|
||||
exploreVehicles: string;
|
||||
surfaceLabel: string;
|
||||
surfaceTitle: string;
|
||||
surfaceBody: string;
|
||||
liveLabel: string;
|
||||
trustedFleets: string;
|
||||
brandedFlows: string;
|
||||
multiTenant: string;
|
||||
companyKicker: string;
|
||||
companyTitle: string;
|
||||
companyBody: string;
|
||||
renterKicker: string;
|
||||
renterTitle: string;
|
||||
renterBody: string;
|
||||
pillars: MarketplaceHomepagePillar[];
|
||||
metrics: MarketplaceHomepageMetric[];
|
||||
featureLabel: string;
|
||||
features: string[];
|
||||
stepsTitle: string;
|
||||
steps: MarketplaceHomepageStep[];
|
||||
stepLabel: string;
|
||||
readyKicker: string;
|
||||
readyTitle: string;
|
||||
readyBody: string;
|
||||
viewPricing: string;
|
||||
createWorkspace: string;
|
||||
};
|
||||
export type MarketplaceHomepageConfig = Record<MarketplaceLanguage, MarketplaceHomepageContent>;
|
||||
export declare const defaultMarketplaceHomepageSections: MarketplaceHomepageSectionType[];
|
||||
export declare const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig;
|
||||
export declare function cloneMarketplaceHomepageContent(): MarketplaceHomepageConfig;
|
||||
export declare function resolveMarketplaceHomepageSections(sections?: MarketplaceHomepageSectionType[] | null): MarketplaceHomepageSectionType[];
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.defaultMarketplaceHomepageContent = exports.defaultMarketplaceHomepageSections = void 0;
|
||||
exports.cloneMarketplaceHomepageContent = cloneMarketplaceHomepageContent;
|
||||
exports.resolveMarketplaceHomepageSections = resolveMarketplaceHomepageSections;
|
||||
exports.defaultMarketplaceHomepageSections = [
|
||||
'hero',
|
||||
'surface',
|
||||
'pillars',
|
||||
'audiences',
|
||||
'features',
|
||||
'steps',
|
||||
'closing',
|
||||
];
|
||||
exports.defaultMarketplaceHomepageContent = {
|
||||
en: {
|
||||
sections: exports.defaultMarketplaceHomepageSections,
|
||||
heroKicker: 'RentalDriveGo',
|
||||
heroTitle: 'Marketplace discovery with a sharper front door.',
|
||||
heroBody: 'Rental companies run private operations, renters browse a shared marketplace, and every booking ends on the company’s own branded checkout.',
|
||||
startTrial: 'Start free trial',
|
||||
exploreVehicles: 'Explore vehicles',
|
||||
surfaceLabel: 'Marketplace surface',
|
||||
surfaceTitle: 'Designed for two audiences at once.',
|
||||
surfaceBody: 'Operators need control, renters need confidence. The marketplace should show both without feeling like a template.',
|
||||
liveLabel: 'Live network',
|
||||
trustedFleets: 'Trusted fleets',
|
||||
brandedFlows: 'Branded booking flows',
|
||||
multiTenant: 'Multi-tenant operations',
|
||||
companyKicker: 'Operator workflow',
|
||||
companyTitle: 'Manage inventory, offers, billing, and staff from one control layer.',
|
||||
companyBody: 'Publish inventory once, decide what appears publicly, and keep contracts, payments, and reporting isolated per company.',
|
||||
renterKicker: 'Renter experience',
|
||||
renterTitle: 'Let renters compare quickly, then hand them off to the right company site.',
|
||||
renterBody: 'The marketplace works as a discovery engine, not a dead-end aggregator. Search here, reserve there, and pay direct.',
|
||||
pillars: [
|
||||
['Unified publishing', 'Vehicle photos, pricing, and offers flow from one admin workflow into marketplace discovery and branded booking pages.'],
|
||||
['Qualified discovery', 'Featured offers, location-aware browsing, and company reputation signals help renters narrow options fast.'],
|
||||
['Direct revenue path', 'Bookings move into the company’s own payment flow, so customer ownership and cash collection stay with the business.'],
|
||||
].map(([title, body]) => ({ title: title, body: body })),
|
||||
metrics: [
|
||||
['01', 'Shared marketplace visibility'],
|
||||
['02', 'Direct company checkout'],
|
||||
['03', 'Company-owned payments'],
|
||||
].map(([value, label]) => ({ value: value, label: label })),
|
||||
featureLabel: 'What the product covers',
|
||||
features: [
|
||||
'Fleet management with multi-photo uploads',
|
||||
'Marketplace offers and redirect booking flow',
|
||||
'Branded public booking site per company',
|
||||
'Customer CRM, analytics, and billing controls',
|
||||
],
|
||||
stepsTitle: 'How companies launch',
|
||||
steps: [
|
||||
['1', 'Create your company workspace', 'Pick a plan, launch your 90-day trial, and verify the owner account.'],
|
||||
['2', 'Publish vehicles and offers', 'Upload photos once and control what appears on the marketplace and branded site.'],
|
||||
['3', 'Accept bookings on your own site', 'Renters discover your fleet on RentalDriveGo, then pay you directly on your branded booking flow.'],
|
||||
].map(([step, title, body]) => ({ step: step, title: title, body: body })),
|
||||
stepLabel: 'Step',
|
||||
readyKicker: 'Ready to launch',
|
||||
readyTitle: 'The marketplace homepage should explain the product in seconds.',
|
||||
readyBody: 'Use the marketplace to attract demand, then move renters into a branded experience that keeps pricing, payments, and trust tied to your company.',
|
||||
viewPricing: 'View pricing',
|
||||
createWorkspace: 'Create workspace',
|
||||
},
|
||||
fr: {
|
||||
sections: exports.defaultMarketplaceHomepageSections,
|
||||
heroKicker: 'RentalDriveGo',
|
||||
heroTitle: 'Une vitrine marketplace plus claire et plus percutante.',
|
||||
heroBody: "Les entreprises de location gèrent leurs opérations en privé, les clients parcourent une marketplace commune et chaque réservation se finalise sur le parcours de paiement aux couleurs de l'entreprise.",
|
||||
startTrial: 'Commencer l’essai gratuit',
|
||||
exploreVehicles: 'Explorer les véhicules',
|
||||
surfaceLabel: 'Vitrine marketplace',
|
||||
surfaceTitle: 'Pensée pour deux audiences à la fois.',
|
||||
surfaceBody: 'Les opérateurs veulent du contrôle, les clients veulent de la confiance. La marketplace doit montrer les deux sans ressembler à un modèle générique.',
|
||||
liveLabel: 'Réseau actif',
|
||||
trustedFleets: 'Flottes fiables',
|
||||
brandedFlows: 'Parcours de marque',
|
||||
multiTenant: 'Opérations multi-tenant',
|
||||
companyKicker: 'Flux opérateur',
|
||||
companyTitle: 'Pilotez l’inventaire, les offres, la facturation et l’équipe depuis un seul centre de contrôle.',
|
||||
companyBody: 'Publiez une seule fois, choisissez ce qui apparaît publiquement, et gardez contrats, paiements et rapports isolés par entreprise.',
|
||||
renterKicker: 'Expérience client',
|
||||
renterTitle: 'Laissez les clients comparer rapidement, puis redirigez-les vers le bon site d’entreprise.',
|
||||
renterBody: 'La marketplace sert de moteur de découverte, pas d’agrégateur sans suite. Recherche ici, réservation là-bas, paiement direct.',
|
||||
pillars: [
|
||||
['Publication unifiée', 'Les photos, tarifs et offres circulent depuis un seul flux d’administration vers la découverte marketplace et les pages de réservation de marque.'],
|
||||
['Découverte qualifiée', 'Offres mises en avant, navigation par localisation et signaux de réputation aident les clients à filtrer rapidement.'],
|
||||
['Revenus en direct', 'Les réservations basculent vers le paiement propre à l’entreprise afin de préserver la relation client et l’encaissement.'],
|
||||
].map(([title, body]) => ({ title: title, body: body })),
|
||||
metrics: [
|
||||
['01', 'Visibilité marketplace partagée'],
|
||||
['02', 'Paiement direct entreprise'],
|
||||
['03', 'Paiements gérés par l’entreprise'],
|
||||
].map(([value, label]) => ({ value: value, label: label })),
|
||||
featureLabel: 'Ce que couvre le produit',
|
||||
features: [
|
||||
'Gestion de flotte avec téléversement de plusieurs photos',
|
||||
'Offres marketplace et redirection vers la réservation',
|
||||
'Site public de réservation par entreprise',
|
||||
'CRM client, analytics et contrôle de facturation',
|
||||
],
|
||||
stepsTitle: 'Comment les entreprises démarrent',
|
||||
steps: [
|
||||
['1', 'Créez votre espace entreprise', 'Choisissez un plan, lancez votre essai de 90 jours et vérifiez le compte propriétaire.'],
|
||||
['2', 'Publiez véhicules et offres', 'Téléversez une seule fois et contrôlez ce qui apparaît sur la marketplace et le site de marque.'],
|
||||
['3', 'Acceptez les réservations sur votre site', 'Les clients découvrent votre flotte sur RentalDriveGo puis paient directement via votre parcours de réservation.'],
|
||||
].map(([step, title, body]) => ({ step: step, title: title, body: body })),
|
||||
stepLabel: 'Étape',
|
||||
readyKicker: 'Prêt à démarrer',
|
||||
readyTitle: 'La page d’accueil marketplace doit présenter le produit en quelques secondes.',
|
||||
readyBody: 'Utilisez la marketplace pour capter la demande, puis orientez les clients vers une expérience de marque où les prix, les paiements et la confiance restent attachés à votre entreprise.',
|
||||
viewPricing: 'Voir les tarifs',
|
||||
createWorkspace: 'Créer l’espace',
|
||||
},
|
||||
ar: {
|
||||
sections: exports.defaultMarketplaceHomepageSections,
|
||||
heroKicker: 'RentalDriveGo',
|
||||
heroTitle: 'واجهة سوق أكثر وضوحاً وتأثيراً.',
|
||||
heroBody: 'شركات التأجير تدير عملياتها بشكل خاص، والمستأجرون يتصفحون سوقاً مشتركاً، وكل حجز ينتهي في صفحة دفع تحمل هوية الشركة ذاتها.',
|
||||
startTrial: 'ابدأ التجربة المجانية',
|
||||
exploreVehicles: 'استكشف السيارات',
|
||||
surfaceLabel: 'واجهة السوق',
|
||||
surfaceTitle: 'مصممة لجمهورين في الوقت نفسه.',
|
||||
surfaceBody: 'المشغلون يريدون التحكم، والمستأجرون يريدون الثقة. يجب أن تُظهر المنصة الأمرين معاً من دون أن تبدو كقالب عادي.',
|
||||
liveLabel: 'شبكة نشطة',
|
||||
trustedFleets: 'أساطيل موثوقة',
|
||||
brandedFlows: 'مسارات حجز مخصصة',
|
||||
multiTenant: 'عمليات متعددة الشركات',
|
||||
companyKicker: 'سير عمل المشغل',
|
||||
companyTitle: 'تحكم في المخزون والعروض والفوترة والفريق من طبقة تشغيل واحدة.',
|
||||
companyBody: 'انشر المخزون مرة واحدة، وحدد ما يظهر للعامة، واحتفظ بالعقود والمدفوعات والتقارير معزولة لكل شركة.',
|
||||
renterKicker: 'تجربة المستأجر',
|
||||
renterTitle: 'دع المستأجرين يقارنون بسرعة ثم وجّههم إلى موقع الشركة المناسب.',
|
||||
renterBody: 'هذه المنصة محرك لاكتشاف الخيارات، وليست مُجمِّعاً بلا مسار واضح. ابحث هنا، احجز هناك، وادفع مباشرة للشركة.',
|
||||
pillars: [
|
||||
['نشر موحد', 'صور السيارات والأسعار والعروض تنتقل من سير إدارة واحد إلى السوق وصفحات الحجز ذات الهوية الخاصة.'],
|
||||
['اكتشاف مؤهل', 'تساعد العروض المميزة، والتصفح حسب الموقع، وإشارات السمعة المستأجرين على تحديد خياراتهم بسرعة.'],
|
||||
['مسار إيراد مباشر', 'تنتقل الحجوزات إلى صفحة الدفع الخاصة بالشركة حتى تبقى علاقة العميل والتحصيل المالي بيد الشركة نفسها.'],
|
||||
].map(([title, body]) => ({ title: title, body: body })),
|
||||
metrics: [
|
||||
['01', 'ظهور مشترك في السوق'],
|
||||
['02', 'دفع مباشر للشركة'],
|
||||
['03', 'مدفوعات مملوكة للشركة'],
|
||||
].map(([value, label]) => ({ value: value, label: label })),
|
||||
featureLabel: 'ما الذي يقدمه المنتج',
|
||||
features: [
|
||||
'إدارة الأسطول مع رفع عدة صور',
|
||||
'عروض السوق والتحويل إلى مسار الحجز',
|
||||
'موقع حجز عام مخصص لكل شركة',
|
||||
'إدارة العملاء والتحليلات والفوترة',
|
||||
],
|
||||
stepsTitle: 'كيف تبدأ الشركات',
|
||||
steps: [
|
||||
['1', 'أنشئ مساحة عمل شركتك', 'اختر الخطة وابدأ تجربتك لمدة 14 يوماً ثم فعّل حساب المالك.'],
|
||||
['2', 'انشر السيارات والعروض', 'ارفع الصور مرة واحدة وتحكم فيما يظهر في السوق وفي الموقع المخصص.'],
|
||||
['3', 'استقبل الحجوزات على موقعك', 'يكتشف المستأجرون أسطولك على RentalDriveGo ثم يدفعون مباشرة عبر مسار الحجز الخاص بك.'],
|
||||
].map(([step, title, body]) => ({ step: step, title: title, body: body })),
|
||||
stepLabel: 'الخطوة',
|
||||
readyKicker: 'جاهز للانطلاق',
|
||||
readyTitle: 'يجب أن تشرح الصفحة الرئيسية قيمة المنصة خلال ثوانٍ.',
|
||||
readyBody: 'استخدم السوق لجذب الطلب، ثم انقل المستأجرين إلى تجربة تحمل هوية شركتك، بحيث تبقى الأسعار والمدفوعات والثقة مرتبطة بنشاطك.',
|
||||
viewPricing: 'عرض الأسعار',
|
||||
createWorkspace: 'إنشاء المساحة',
|
||||
},
|
||||
};
|
||||
function cloneMarketplaceHomepageContent() {
|
||||
return JSON.parse(JSON.stringify(exports.defaultMarketplaceHomepageContent));
|
||||
}
|
||||
function resolveMarketplaceHomepageSections(sections) {
|
||||
const source = sections?.length ? sections : exports.defaultMarketplaceHomepageSections;
|
||||
return source.filter((section, index) => source.indexOf(section) === index);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@rentaldrivego/types",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Standard API response shapes
|
||||
|
||||
export interface ApiSuccess<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface ApiPaginated<T> {
|
||||
data: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
error: string
|
||||
message: string
|
||||
statusCode: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
// Plan prices in smallest currency unit (MAD, in centimes)
|
||||
export const PLAN_PRICES: Record<string, Record<string, Record<string, number>>> = {
|
||||
STARTER: {
|
||||
MONTHLY: { MAD: 2990 },
|
||||
ANNUAL: { MAD: 29900 },
|
||||
},
|
||||
GROWTH: {
|
||||
MONTHLY: { MAD: 3990 },
|
||||
ANNUAL: { MAD: 39900 },
|
||||
},
|
||||
PRO: {
|
||||
MONTHLY: { MAD: 99900 },
|
||||
ANNUAL: { MAD: 999000 },
|
||||
},
|
||||
}
|
||||
|
||||
export const PLAN_FEATURES: Record<string, string[]> = {
|
||||
STARTER: [
|
||||
'Up to 10 vehicles',
|
||||
'1 user account',
|
||||
'Basic analytics',
|
||||
'Marketplace listing',
|
||||
],
|
||||
GROWTH: [
|
||||
'Up to 50 vehicles',
|
||||
'5 user accounts',
|
||||
'Full analytics',
|
||||
'Priority marketplace placement',
|
||||
'Custom branding',
|
||||
],
|
||||
PRO: [
|
||||
'Unlimited vehicles',
|
||||
'Unlimited user accounts',
|
||||
'Advanced reports',
|
||||
'API access',
|
||||
'Dedicated support',
|
||||
],
|
||||
}
|
||||
|
||||
export type Locale = 'en' | 'fr' | 'ar'
|
||||
export type SupportedCurrency = 'MAD'
|
||||
|
||||
function resolveLocale(locale?: Locale): Locale {
|
||||
if (locale) return locale
|
||||
|
||||
const lang =
|
||||
typeof globalThis === 'object' && 'document' in globalThis
|
||||
? (globalThis as { document?: { documentElement?: { lang?: string } } }).document?.documentElement?.lang
|
||||
: undefined
|
||||
|
||||
if (lang === 'en' || lang === 'fr' || lang === 'ar') {
|
||||
return lang
|
||||
}
|
||||
|
||||
return 'en'
|
||||
}
|
||||
|
||||
export function getCurrencyLabel(locale?: Locale, _currency: SupportedCurrency = 'MAD'): string {
|
||||
return resolveLocale(locale) === 'ar' ? 'درهم' : 'MAD'
|
||||
}
|
||||
|
||||
export function formatCurrency(amount: number, _currency: SupportedCurrency = 'MAD', locale?: Locale): string {
|
||||
const resolvedLocale = resolveLocale(locale)
|
||||
const divisor = 100
|
||||
const value = amount / divisor
|
||||
const localeMap: Record<Locale, string> = { en: 'en-US', fr: 'fr-FR', ar: 'ar-MA' }
|
||||
const formattedValue = new Intl.NumberFormat(localeMap[resolvedLocale], {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value)
|
||||
const currencyLabel = getCurrencyLabel(resolvedLocale, _currency)
|
||||
|
||||
return resolvedLocale === 'en'
|
||||
? `${currencyLabel} ${formattedValue}`
|
||||
: `${formattedValue} ${currencyLabel}`
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
export interface DamageZone {
|
||||
zone: VehicleZone
|
||||
severity: 'SCRATCH' | 'DENT' | 'CRACK' | 'MISSING' | 'OTHER'
|
||||
note?: string
|
||||
}
|
||||
|
||||
export type VehicleZone =
|
||||
| 'hood' | 'roof' | 'trunk'
|
||||
| 'front-left-door' | 'front-right-door'
|
||||
| 'rear-left-door' | 'rear-right-door'
|
||||
| 'front-left-fender' | 'front-right-fender'
|
||||
| 'rear-left-fender' | 'rear-right-fender'
|
||||
| 'front-bumper' | 'rear-bumper'
|
||||
| 'windshield' | 'rear-window'
|
||||
| 'left-mirror' | 'right-mirror'
|
||||
| 'front-left-wheel' | 'front-right-wheel'
|
||||
| 'rear-left-wheel' | 'rear-right-wheel'
|
||||
| 'interior' | 'under-hood' | 'undercarriage'
|
||||
|
||||
export const DAMAGE_ZONE_COORDS: Record<VehicleZone, { x: number; y: number; w: number; h: number }> = {
|
||||
'hood': { x: 48, y: 8, w: 64, h: 28 },
|
||||
'roof': { x: 48, y: 52, w: 64, h: 56 },
|
||||
'trunk': { x: 48, y: 124, w: 64, h: 28 },
|
||||
'front-bumper': { x: 48, y: 0, w: 64, h: 10 },
|
||||
'rear-bumper': { x: 48, y: 150, w: 64, h: 10 },
|
||||
'front-left-fender': { x: 20, y: 10, w: 30, h: 35 },
|
||||
'front-right-fender': { x: 110, y: 10, w: 30, h: 35 },
|
||||
'front-left-door': { x: 20, y: 52, w: 30, h: 30 },
|
||||
'front-right-door': { x: 110, y: 52, w: 30, h: 30 },
|
||||
'rear-left-door': { x: 20, y: 82, w: 30, h: 30 },
|
||||
'rear-right-door': { x: 110, y: 82, w: 30, h: 30 },
|
||||
'rear-left-fender': { x: 20, y: 112, w: 30, h: 35 },
|
||||
'rear-right-fender': { x: 110, y: 112, w: 30, h: 35 },
|
||||
'windshield': { x: 52, y: 38, w: 56, h: 18 },
|
||||
'rear-window': { x: 52, y: 104, w: 56, h: 18 },
|
||||
'left-mirror': { x: 8, y: 50, w: 14, h: 10 },
|
||||
'right-mirror': { x: 138, y: 50, w: 14, h: 10 },
|
||||
'front-left-wheel': { x: 10, y: 24, w: 22, h: 26 },
|
||||
'front-right-wheel': { x: 128, y: 24, w: 22, h: 26 },
|
||||
'rear-left-wheel': { x: 10, y: 110, w: 22, h: 26 },
|
||||
'rear-right-wheel': { x: 128, y: 110, w: 22, h: 26 },
|
||||
'interior': { x: 55, y: 58, w: 50, h: 44 },
|
||||
'under-hood': { x: 52, y: 10, w: 56, h: 20 },
|
||||
'undercarriage': { x: 52, y: 130, w: 56, h: 20 },
|
||||
}
|
||||
|
||||
export const SEVERITY_COLORS: Record<DamageZone['severity'], string> = {
|
||||
SCRATCH: '#F59E0B',
|
||||
DENT: '#EF4444',
|
||||
CRACK: '#8B5CF6',
|
||||
MISSING: '#374151',
|
||||
OTHER: '#6B7280',
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export type FuelPolicyType = 'FULL_TO_FULL' | 'FULL_TO_EMPTY' | 'SAME_TO_SAME' | 'PREPAID' | 'FREE'
|
||||
|
||||
export const FUEL_POLICY_LABELS: Record<FuelPolicyType, Record<'en' | 'fr' | 'ar', string>> = {
|
||||
FULL_TO_FULL: {
|
||||
en: 'Full-to-Full: Return vehicle with a full tank. A refueling fee applies if returned with less fuel.',
|
||||
fr: "Plein à plein : retournez le véhicule avec le plein. Des frais de ravitaillement s'appliquent sinon.",
|
||||
ar: 'ممتلئ إلى ممتلئ: يجب إعادة السيارة بخزان ممتلئ. رسوم إضافية في حال الإعادة بمستوى أقل.',
|
||||
},
|
||||
FULL_TO_EMPTY: {
|
||||
en: 'Full-to-Empty: Vehicle is provided with a full tank. No refund for unused fuel upon return.',
|
||||
fr: "Plein à vide : le véhicule est livré avec le plein. Aucun remboursement n'est accordé pour le carburant non utilisé.",
|
||||
ar: 'ممتلئ إلى فارغ: تُسلَّم السيارة بخزان ممتلئ. لا استرداد للوقود غير المستخدم.',
|
||||
},
|
||||
SAME_TO_SAME: {
|
||||
en: 'Same-to-Same: Return vehicle with the same fuel level as at pickup.',
|
||||
fr: "Même niveau : retournez le véhicule avec le même niveau de carburant qu'au départ.",
|
||||
ar: 'نفس المستوى: أعد السيارة بنفس مستوى الوقود عند الاستلام.',
|
||||
},
|
||||
PREPAID: {
|
||||
en: 'Prepaid Fuel: You pre-purchase a full tank at a fixed rate. Return at any fuel level.',
|
||||
fr: "Carburant prépayé : vous achetez un plein à tarif fixe à l'avance. Retour possible avec n'importe quel niveau.",
|
||||
ar: 'الوقود المدفوع مسبقاً: تدفع مقدماً ثمن خزان كامل بسعر ثابت. يمكن إعادة السيارة بأي مستوى.',
|
||||
},
|
||||
FREE: {
|
||||
en: 'Fuel Included: Fuel cost is included in the rental price.',
|
||||
fr: 'Carburant inclus : le coût du carburant est inclus dans le tarif de location.',
|
||||
ar: 'الوقود مشمول: تكلفة الوقود مشمولة في سعر الإيجار.',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './damage'
|
||||
export * from './fuel'
|
||||
export * from './api'
|
||||
export * from './marketplace-homepage'
|
||||
@@ -0,0 +1,251 @@
|
||||
export type MarketplaceLanguage = 'en' | 'fr' | 'ar'
|
||||
|
||||
export type MarketplaceHomepageMetric = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export type MarketplaceHomepagePillar = {
|
||||
title: string
|
||||
body: string
|
||||
}
|
||||
|
||||
export type MarketplaceHomepageStep = {
|
||||
step: string
|
||||
title: string
|
||||
body: string
|
||||
}
|
||||
|
||||
export type MarketplaceHomepageSectionType =
|
||||
| 'hero'
|
||||
| 'surface'
|
||||
| 'pillars'
|
||||
| 'audiences'
|
||||
| 'features'
|
||||
| 'steps'
|
||||
| 'closing'
|
||||
|
||||
export type MarketplaceHomepageContent = {
|
||||
sections: MarketplaceHomepageSectionType[]
|
||||
heroKicker: string
|
||||
heroTitle: string
|
||||
heroBody: string
|
||||
startTrial: string
|
||||
exploreVehicles: string
|
||||
surfaceLabel: string
|
||||
surfaceTitle: string
|
||||
surfaceBody: string
|
||||
liveLabel: string
|
||||
trustedFleets: string
|
||||
brandedFlows: string
|
||||
multiTenant: string
|
||||
companyKicker: string
|
||||
companyTitle: string
|
||||
companyBody: string
|
||||
renterKicker: string
|
||||
renterTitle: string
|
||||
renterBody: string
|
||||
pillars: MarketplaceHomepagePillar[]
|
||||
metrics: MarketplaceHomepageMetric[]
|
||||
featureLabel: string
|
||||
features: string[]
|
||||
stepsTitle: string
|
||||
steps: MarketplaceHomepageStep[]
|
||||
stepLabel: string
|
||||
readyKicker: string
|
||||
readyTitle: string
|
||||
readyBody: string
|
||||
viewPricing: string
|
||||
createWorkspace: string
|
||||
}
|
||||
|
||||
export type MarketplaceHomepageConfig = Record<MarketplaceLanguage, MarketplaceHomepageContent>
|
||||
|
||||
export const defaultMarketplaceHomepageSections: MarketplaceHomepageSectionType[] = [
|
||||
'hero',
|
||||
'surface',
|
||||
'pillars',
|
||||
'audiences',
|
||||
'features',
|
||||
'steps',
|
||||
'closing',
|
||||
]
|
||||
|
||||
export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = {
|
||||
en: {
|
||||
sections: defaultMarketplaceHomepageSections,
|
||||
heroKicker: 'RentalDriveGo',
|
||||
heroTitle: 'Marketplace discovery with a sharper front door.',
|
||||
heroBody:
|
||||
'Rental companies run private operations, renters browse a shared marketplace, and every booking ends on the company’s own branded checkout.',
|
||||
startTrial: 'Start free trial',
|
||||
exploreVehicles: 'Explore vehicles',
|
||||
surfaceLabel: 'Marketplace surface',
|
||||
surfaceTitle: 'Designed for two audiences at once.',
|
||||
surfaceBody:
|
||||
'Operators need control, renters need confidence. The marketplace should show both without feeling like a template.',
|
||||
liveLabel: 'Live network',
|
||||
trustedFleets: 'Trusted fleets',
|
||||
brandedFlows: 'Branded booking flows',
|
||||
multiTenant: 'Multi-tenant operations',
|
||||
companyKicker: 'Operator workflow',
|
||||
companyTitle: 'Manage inventory, offers, billing, and staff from one control layer.',
|
||||
companyBody:
|
||||
'Publish inventory once, decide what appears publicly, and keep contracts, payments, and reporting isolated per company.',
|
||||
renterKicker: 'Renter experience',
|
||||
renterTitle: 'Let renters compare quickly, then hand them off to the right company site.',
|
||||
renterBody:
|
||||
'The marketplace works as a discovery engine, not a dead-end aggregator. Search here, reserve there, and pay direct.',
|
||||
pillars: [
|
||||
['Unified publishing', 'Vehicle photos, pricing, and offers flow from one admin workflow into marketplace discovery and branded booking pages.'],
|
||||
['Qualified discovery', 'Featured offers, location-aware browsing, and company reputation signals help renters narrow options fast.'],
|
||||
['Direct revenue path', 'Bookings move into the company’s own payment flow, so customer ownership and cash collection stay with the business.'],
|
||||
].map(([title, body]) => ({ title: title!, body: body! })),
|
||||
metrics: [
|
||||
['01', 'Shared marketplace visibility'],
|
||||
['02', 'Direct company checkout'],
|
||||
['03', 'Company-owned payments'],
|
||||
].map(([value, label]) => ({ value: value!, label: label! })),
|
||||
featureLabel: 'What the product covers',
|
||||
features: [
|
||||
'Fleet management with multi-photo uploads',
|
||||
'Marketplace offers and redirect booking flow',
|
||||
'Branded public booking site per company',
|
||||
'Customer CRM, analytics, and billing controls',
|
||||
],
|
||||
stepsTitle: 'How companies launch',
|
||||
steps: [
|
||||
['1', 'Create your company workspace', 'Pick a plan, launch your 90-day trial, and verify the owner account.'],
|
||||
['2', 'Publish vehicles and offers', 'Upload photos once and control what appears on the marketplace and branded site.'],
|
||||
['3', 'Accept bookings on your own site', 'Renters discover your fleet on RentalDriveGo, then pay you directly on your branded booking flow.'],
|
||||
].map(([step, title, body]) => ({ step: step!, title: title!, body: body! })),
|
||||
stepLabel: 'Step',
|
||||
readyKicker: 'Ready to launch',
|
||||
readyTitle: 'The marketplace homepage should explain the product in seconds.',
|
||||
readyBody:
|
||||
'Use the marketplace to attract demand, then move renters into a branded experience that keeps pricing, payments, and trust tied to your company.',
|
||||
viewPricing: 'View pricing',
|
||||
createWorkspace: 'Create workspace',
|
||||
},
|
||||
fr: {
|
||||
sections: defaultMarketplaceHomepageSections,
|
||||
heroKicker: 'RentalDriveGo',
|
||||
heroTitle: 'Une vitrine marketplace plus claire et plus percutante.',
|
||||
heroBody:
|
||||
"Les entreprises de location gèrent leurs opérations en privé, les clients parcourent une marketplace commune et chaque réservation se finalise sur le parcours de paiement aux couleurs de l'entreprise.",
|
||||
startTrial: 'Commencer l’essai gratuit',
|
||||
exploreVehicles: 'Explorer les véhicules',
|
||||
surfaceLabel: 'Vitrine marketplace',
|
||||
surfaceTitle: 'Pensée pour deux audiences à la fois.',
|
||||
surfaceBody:
|
||||
'Les opérateurs veulent du contrôle, les clients veulent de la confiance. La marketplace doit montrer les deux sans ressembler à un modèle générique.',
|
||||
liveLabel: 'Réseau actif',
|
||||
trustedFleets: 'Flottes fiables',
|
||||
brandedFlows: 'Parcours de marque',
|
||||
multiTenant: 'Opérations multi-tenant',
|
||||
companyKicker: 'Flux opérateur',
|
||||
companyTitle: 'Pilotez l’inventaire, les offres, la facturation et l’équipe depuis un seul centre de contrôle.',
|
||||
companyBody:
|
||||
'Publiez une seule fois, choisissez ce qui apparaît publiquement, et gardez contrats, paiements et rapports isolés par entreprise.',
|
||||
renterKicker: 'Expérience client',
|
||||
renterTitle: 'Laissez les clients comparer rapidement, puis redirigez-les vers le bon site d’entreprise.',
|
||||
renterBody:
|
||||
'La marketplace sert de moteur de découverte, pas d’agrégateur sans suite. Recherche ici, réservation là-bas, paiement direct.',
|
||||
pillars: [
|
||||
['Publication unifiée', 'Les photos, tarifs et offres circulent depuis un seul flux d’administration vers la découverte marketplace et les pages de réservation de marque.'],
|
||||
['Découverte qualifiée', 'Offres mises en avant, navigation par localisation et signaux de réputation aident les clients à filtrer rapidement.'],
|
||||
['Revenus en direct', 'Les réservations basculent vers le paiement propre à l’entreprise afin de préserver la relation client et l’encaissement.'],
|
||||
].map(([title, body]) => ({ title: title!, body: body! })),
|
||||
metrics: [
|
||||
['01', 'Visibilité marketplace partagée'],
|
||||
['02', 'Paiement direct entreprise'],
|
||||
['03', 'Paiements gérés par l’entreprise'],
|
||||
].map(([value, label]) => ({ value: value!, label: label! })),
|
||||
featureLabel: 'Ce que couvre le produit',
|
||||
features: [
|
||||
'Gestion de flotte avec téléversement de plusieurs photos',
|
||||
'Offres marketplace et redirection vers la réservation',
|
||||
'Site public de réservation par entreprise',
|
||||
'CRM client, analytics et contrôle de facturation',
|
||||
],
|
||||
stepsTitle: 'Comment les entreprises démarrent',
|
||||
steps: [
|
||||
['1', 'Créez votre espace entreprise', 'Choisissez un plan, lancez votre essai de 90 jours et vérifiez le compte propriétaire.'],
|
||||
['2', 'Publiez véhicules et offres', 'Téléversez une seule fois et contrôlez ce qui apparaît sur la marketplace et le site de marque.'],
|
||||
['3', 'Acceptez les réservations sur votre site', 'Les clients découvrent votre flotte sur RentalDriveGo puis paient directement via votre parcours de réservation.'],
|
||||
].map(([step, title, body]) => ({ step: step!, title: title!, body: body! })),
|
||||
stepLabel: 'Étape',
|
||||
readyKicker: 'Prêt à démarrer',
|
||||
readyTitle: 'La page d’accueil marketplace doit présenter le produit en quelques secondes.',
|
||||
readyBody:
|
||||
'Utilisez la marketplace pour capter la demande, puis orientez les clients vers une expérience de marque où les prix, les paiements et la confiance restent attachés à votre entreprise.',
|
||||
viewPricing: 'Voir les tarifs',
|
||||
createWorkspace: 'Créer l’espace',
|
||||
},
|
||||
ar: {
|
||||
sections: defaultMarketplaceHomepageSections,
|
||||
heroKicker: 'RentalDriveGo',
|
||||
heroTitle: 'واجهة سوق أكثر وضوحاً وتأثيراً.',
|
||||
heroBody:
|
||||
'شركات التأجير تدير عملياتها بشكل خاص، والمستأجرون يتصفحون سوقاً مشتركاً، وكل حجز ينتهي في صفحة دفع تحمل هوية الشركة ذاتها.',
|
||||
startTrial: 'ابدأ التجربة المجانية',
|
||||
exploreVehicles: 'استكشف السيارات',
|
||||
surfaceLabel: 'واجهة السوق',
|
||||
surfaceTitle: 'مصممة لجمهورين في الوقت نفسه.',
|
||||
surfaceBody:
|
||||
'المشغلون يريدون التحكم، والمستأجرون يريدون الثقة. يجب أن تُظهر المنصة الأمرين معاً من دون أن تبدو كقالب عادي.',
|
||||
liveLabel: 'شبكة نشطة',
|
||||
trustedFleets: 'أساطيل موثوقة',
|
||||
brandedFlows: 'مسارات حجز مخصصة',
|
||||
multiTenant: 'عمليات متعددة الشركات',
|
||||
companyKicker: 'سير عمل المشغل',
|
||||
companyTitle: 'تحكم في المخزون والعروض والفوترة والفريق من طبقة تشغيل واحدة.',
|
||||
companyBody:
|
||||
'انشر المخزون مرة واحدة، وحدد ما يظهر للعامة، واحتفظ بالعقود والمدفوعات والتقارير معزولة لكل شركة.',
|
||||
renterKicker: 'تجربة المستأجر',
|
||||
renterTitle: 'دع المستأجرين يقارنون بسرعة ثم وجّههم إلى موقع الشركة المناسب.',
|
||||
renterBody:
|
||||
'هذه المنصة محرك لاكتشاف الخيارات، وليست مُجمِّعاً بلا مسار واضح. ابحث هنا، احجز هناك، وادفع مباشرة للشركة.',
|
||||
pillars: [
|
||||
['نشر موحد', 'صور السيارات والأسعار والعروض تنتقل من سير إدارة واحد إلى السوق وصفحات الحجز ذات الهوية الخاصة.'],
|
||||
['اكتشاف مؤهل', 'تساعد العروض المميزة، والتصفح حسب الموقع، وإشارات السمعة المستأجرين على تحديد خياراتهم بسرعة.'],
|
||||
['مسار إيراد مباشر', 'تنتقل الحجوزات إلى صفحة الدفع الخاصة بالشركة حتى تبقى علاقة العميل والتحصيل المالي بيد الشركة نفسها.'],
|
||||
].map(([title, body]) => ({ title: title!, body: body! })),
|
||||
metrics: [
|
||||
['01', 'ظهور مشترك في السوق'],
|
||||
['02', 'دفع مباشر للشركة'],
|
||||
['03', 'مدفوعات مملوكة للشركة'],
|
||||
].map(([value, label]) => ({ value: value!, label: label! })),
|
||||
featureLabel: 'ما الذي يقدمه المنتج',
|
||||
features: [
|
||||
'إدارة الأسطول مع رفع عدة صور',
|
||||
'عروض السوق والتحويل إلى مسار الحجز',
|
||||
'موقع حجز عام مخصص لكل شركة',
|
||||
'إدارة العملاء والتحليلات والفوترة',
|
||||
],
|
||||
stepsTitle: 'كيف تبدأ الشركات',
|
||||
steps: [
|
||||
['1', 'أنشئ مساحة عمل شركتك', 'اختر الخطة وابدأ تجربتك لمدة 14 يوماً ثم فعّل حساب المالك.'],
|
||||
['2', 'انشر السيارات والعروض', 'ارفع الصور مرة واحدة وتحكم فيما يظهر في السوق وفي الموقع المخصص.'],
|
||||
['3', 'استقبل الحجوزات على موقعك', 'يكتشف المستأجرون أسطولك على RentalDriveGo ثم يدفعون مباشرة عبر مسار الحجز الخاص بك.'],
|
||||
].map(([step, title, body]) => ({ step: step!, title: title!, body: body! })),
|
||||
stepLabel: 'الخطوة',
|
||||
readyKicker: 'جاهز للانطلاق',
|
||||
readyTitle: 'يجب أن تشرح الصفحة الرئيسية قيمة المنصة خلال ثوانٍ.',
|
||||
readyBody:
|
||||
'استخدم السوق لجذب الطلب، ثم انقل المستأجرين إلى تجربة تحمل هوية شركتك، بحيث تبقى الأسعار والمدفوعات والثقة مرتبطة بنشاطك.',
|
||||
viewPricing: 'عرض الأسعار',
|
||||
createWorkspace: 'إنشاء المساحة',
|
||||
},
|
||||
}
|
||||
|
||||
export function cloneMarketplaceHomepageContent(): MarketplaceHomepageConfig {
|
||||
return JSON.parse(JSON.stringify(defaultMarketplaceHomepageContent)) as MarketplaceHomepageConfig
|
||||
}
|
||||
|
||||
export function resolveMarketplaceHomepageSections(
|
||||
sections?: MarketplaceHomepageSectionType[] | null,
|
||||
): MarketplaceHomepageSectionType[] {
|
||||
const source = sections?.length ? sections : defaultMarketplaceHomepageSections
|
||||
return source.filter((section, index) => source.indexOf(section) === index)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user