add pricing feature
This commit is contained in:
+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;
|
||||
@@ -205,6 +205,28 @@ enum FuelType {
|
||||
HYBRID
|
||||
}
|
||||
|
||||
enum VehiclePricingMode {
|
||||
MANUAL
|
||||
AUTOMATIC
|
||||
}
|
||||
|
||||
enum VehiclePricingRuleType {
|
||||
DATE_RANGE
|
||||
SEASONAL
|
||||
HOLIDAY
|
||||
WEEKEND
|
||||
SPECIAL_EVENT
|
||||
MANUAL_OVERRIDE
|
||||
}
|
||||
|
||||
enum VehiclePriceChangeSource {
|
||||
CONFIG_UPDATE
|
||||
RULE_CREATED
|
||||
RULE_UPDATED
|
||||
RULE_DELETED
|
||||
MANUAL_OVERRIDE
|
||||
}
|
||||
|
||||
enum OfferType {
|
||||
PERCENTAGE
|
||||
FIXED_AMOUNT
|
||||
@@ -996,6 +1018,8 @@ model Vehicle {
|
||||
maintenance MaintenanceLog[]
|
||||
offerVehicles OfferVehicle[]
|
||||
calendarBlocks VehicleCalendarBlock[]
|
||||
pricingConfiguration VehiclePricingConfiguration?
|
||||
priceHistory VehiclePriceHistory[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -1006,6 +1030,80 @@ model Vehicle {
|
||||
@@map("vehicles")
|
||||
}
|
||||
|
||||
model VehiclePricingConfiguration {
|
||||
id String @id @default(cuid())
|
||||
vehicleId String @unique
|
||||
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
|
||||
pricingMode VehiclePricingMode @default(MANUAL)
|
||||
baseDailyRate Int?
|
||||
weeklyRate Int?
|
||||
weekendRate Int?
|
||||
holidayRate Int?
|
||||
monthlyRate Int?
|
||||
longTermDailyRate Int?
|
||||
minimumDailyRate Int?
|
||||
maximumDailyRate Int?
|
||||
maxDailyPriceMovementPct Int @default(10)
|
||||
automaticPricingEnabled Boolean @default(false)
|
||||
approvalRequired Boolean @default(false)
|
||||
|
||||
rules VehiclePricingRule[]
|
||||
history VehiclePriceHistory[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("vehicle_pricing_configurations")
|
||||
}
|
||||
|
||||
model VehiclePricingRule {
|
||||
id String @id @default(cuid())
|
||||
configurationId String
|
||||
configuration VehiclePricingConfiguration @relation(fields: [configurationId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
ruleType VehiclePricingRuleType @default(DATE_RANGE)
|
||||
startDate DateTime
|
||||
endDate DateTime
|
||||
dailyRate Int?
|
||||
weeklyRate Int?
|
||||
weekendRate Int?
|
||||
monthlyRate Int?
|
||||
minDailyRate Int?
|
||||
maxDailyRate Int?
|
||||
automaticAdjustmentPct Int?
|
||||
priceAdjustment Int?
|
||||
isActive Boolean @default(true)
|
||||
sortOrder Int @default(0)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([configurationId])
|
||||
@@index([configurationId, startDate, endDate])
|
||||
@@map("vehicle_pricing_rules")
|
||||
}
|
||||
|
||||
model VehiclePriceHistory {
|
||||
id String @id @default(cuid())
|
||||
configurationId String
|
||||
configuration VehiclePricingConfiguration @relation(fields: [configurationId], references: [id], onDelete: Cascade)
|
||||
vehicleId String
|
||||
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
|
||||
source VehiclePriceChangeSource @default(CONFIG_UPDATE)
|
||||
changedByEmployeeId String?
|
||||
previousDailyRate Int?
|
||||
nextDailyRate Int?
|
||||
previousWeeklyRate Int?
|
||||
nextWeeklyRate Int?
|
||||
note String?
|
||||
effectiveFrom DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([vehicleId, createdAt])
|
||||
@@index([configurationId, createdAt])
|
||||
@@map("vehicle_price_history")
|
||||
}
|
||||
|
||||
model Offer {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
const { spawnSync } = require('node:child_process')
|
||||
const { readdirSync } = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { ensureDatabaseUrl } = require('../src/runtime-config')
|
||||
const {
|
||||
ensureDatabaseUrl,
|
||||
loadDefaultEnvFiles,
|
||||
normalizeDatabaseUrlForExecution,
|
||||
} = require('../src/runtime-config')
|
||||
const { PrismaClient } = require('../generated')
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '../../..')
|
||||
@@ -24,7 +28,9 @@ function run(command, args) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
loadDefaultEnvFiles(repoRoot)
|
||||
ensureDatabaseUrl()
|
||||
normalizeDatabaseUrlForExecution()
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
try {
|
||||
@@ -103,6 +109,9 @@ async function main() {
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,96 @@
|
||||
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() ?? '',
|
||||
@@ -26,4 +117,7 @@ function ensureDatabaseUrl(env = process.env) {
|
||||
|
||||
module.exports = {
|
||||
ensureDatabaseUrl,
|
||||
loadDefaultEnvFiles,
|
||||
loadEnvFile,
|
||||
normalizeDatabaseUrlForExecution,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user