Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | 5x 5x 5x 5x 9x 2x 2x 3x 3x 2x 3x 3x 2x | // Cost Calculator config service.
//
// Reads/writes the single admin-managed pricing config. getConfig falls back to
// DEFAULT_CALCULATOR_CONFIG when no DB row exists (so the calculator works before
// any admin save). updateConfig validates the full shape with zod and bumps the
// version. KV cache invalidation lives in the admin route (matches home-page).
import {
type CalculatorConfig,
DEFAULT_CALCULATOR_CONFIG,
} from "@interioring/utils/calculator";
import { z } from "zod";
import type { Dal } from "../../dal";
export const CALCULATOR_CONFIG_KEY = "default";
const lineItemSchema = z.tuple([
z.string(),
z.string(),
z.number(),
z.string(),
]);
const roomDefSchema = z.object({
name: z.string(),
icon: z.string(),
repeat: z.enum(["once", "perExtraBed", "perBath"]),
items: z.array(lineItemSchema),
});
export const calculatorConfigSchema = z.object({
version: z.number(),
currency: z.literal("INR"),
designFeePct: z.number().min(0),
gstPct: z.number().min(0),
bands: z.object({ full: z.number().min(0), partial: z.number().min(0) }),
ui: z.object({
gated: z.boolean(),
tone: z.enum(["friendly", "expert"]),
chartStyle: z.enum(["donut", "stacked", "bars"]),
accent: z.enum(["terracotta", "sage", "slate"]),
}),
categories: z.record(
z.string(),
z.object({ label: z.string(), short: z.string(), color: z.string() }),
),
cities: z.array(
z.object({
id: z.string(),
name: z.string(),
zone: z.string(),
mult: z.number(),
tag: z.string(),
}),
),
homes: z.array(
z.object({
id: z.string(),
name: z.string(),
beds: z.number(),
sqft: z.number(),
baths: z.number(),
}),
),
tiers: z.array(
z.object({
id: z.string(),
name: z.string(),
mult: z.number(),
blurb: z.string(),
materials: z.string(),
}),
),
works: z.record(
z.string(),
z.object({
label: z.string(),
icon: z.string(),
sub: z.string(),
core: z.boolean().optional(),
}),
),
rooms: z.record(z.string(), roomDefSchema),
timeline: z.object({
baseWeeksByHome: z.record(z.string(), z.number()),
tierAddWeeks: z.record(z.string(), z.number()),
spread: z.number(),
}),
});
export class CalculatorConfigService {
constructor(private dal: Dal) {}
/** Current config, or the bundled default when none has been saved yet. */
async getConfig(): Promise<CalculatorConfig> {
const row = await this.dal.calculatorConfig.findByKey(
CALCULATOR_CONFIG_KEY,
);
return row?.config ?? DEFAULT_CALCULATOR_CONFIG;
}
/**
* Validate + persist a new config. Throws ZodError on a malformed body.
* Version is server-controlled: it's the previous version + 1.
*/
async updateConfig(input: unknown): Promise<CalculatorConfig> {
const parsed = calculatorConfigSchema.parse(input);
const current = await this.dal.calculatorConfig.findByKey(
CALCULATOR_CONFIG_KEY,
);
const version = (current?.version ?? 0) + 1;
const config: CalculatorConfig = { ...parsed, version };
const row = await this.dal.calculatorConfig.upsert(
CALCULATOR_CONFIG_KEY,
config,
version,
);
return row.config;
}
}
|