@voyant-travel/operator-settings 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmD3B,CAAA;AAEF,MAAM,MAAM,gBAAgB,GAAG,OAAO,gBAAgB,CAAC,YAAY,CAAA;AACnE,MAAM,MAAM,mBAAmB,GAAG,OAAO,gBAAgB,CAAC,YAAY,CAAA;AAEtE;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgB1B,CAAA;AAEF,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAC,YAAY,CAAA;AACjE,MAAM,MAAM,kBAAkB,GAAG,OAAO,eAAe,CAAC,YAAY,CAAA;AAEpE;;;GAGG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQtC,CAAA;AAEF,MAAM,MAAM,2BAA2B,GAAG,OAAO,2BAA2B,CAAC,YAAY,CAAA;AACzF,MAAM,MAAM,8BAA8B,GAAG,OAAO,2BAA2B,CAAC,YAAY,CAAA;AAE5F;;;;GAIG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAOlC,CAAA;AAEF,MAAM,MAAM,uBAAuB,GAAG,OAAO,uBAAuB,CAAC,YAAY,CAAA;AACjF,MAAM,MAAM,0BAA0B,GAAG,OAAO,uBAAuB,CAAC,YAAY,CAAA;AAEpF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAM7B,CAAA;AAEF,MAAM,MAAM,kBAAkB,GAAG,OAAO,kBAAkB,CAAC,YAAY,CAAA;AACvE,MAAM,MAAM,qBAAqB,GAAG,OAAO,kBAAkB,CAAC,YAAY,CAAA"}
package/dist/schema.js ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * `@voyant-travel/operator-settings` drizzle schema — the operator-tenant
3
+ * identity + payment + booking-tax configuration tables.
4
+ *
5
+ * Generic to any deployment (every operator has a profile, payment
6
+ * instructions/defaults, and tax settings); the rows are per-deployment data.
7
+ * A deployment lists this package in `voyant.config` `additionalSchemas` so its
8
+ * tables fold into the combined migration history. Table names + TypeID
9
+ * prefixes are unchanged from the prior starter-local schema (migration parity).
10
+ */
11
+ import { typeId } from "@voyant-travel/db/lib/typeid-column";
12
+ import { jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
13
+ /**
14
+ * Original catch-all table from the first booking-journey settings
15
+ * pass. Runtime code reads/writes the narrower tables below:
16
+ * `operator_profile`, `operator_payment_instructions`,
17
+ * `operator_payment_defaults`, and `booking_tax_settings`.
18
+ */
19
+ export const operatorSettings = pgTable("operator_settings", {
20
+ id: typeId("operator_settings"),
21
+ // Identity
22
+ name: text("name"),
23
+ legalName: text("legal_name"),
24
+ vatId: text("vat_id"),
25
+ registrationNumber: text("registration_number"),
26
+ // Contact
27
+ address: text("address"),
28
+ phone: text("phone"),
29
+ email: text("email"),
30
+ website: text("website"),
31
+ // Banking
32
+ iban: text("iban"),
33
+ bank: text("bank"),
34
+ // Travel licensing — generic enough to cover tourism authority
35
+ // numbers (ANPC, ABTA, IATA), hotel star-rating registries, and
36
+ // cruise flag-state numbers.
37
+ license: text("license"),
38
+ licenseAuthority: text("license_authority"),
39
+ // Signing officer for contracts (the human whose name appears on
40
+ // the operator-side signature line of a staff-issued contract).
41
+ signatoryName: text("signatory_name"),
42
+ signatoryRole: text("signatory_role"),
43
+ /**
44
+ * Default customer-facing payment policy applied when no per-supplier
45
+ * / per-category / per-listing / per-booking override exists.
46
+ *
47
+ * Shape mirrors the `PaymentPolicy` interface in `@voyant-travel/finance`.
48
+ * Stored as jsonb so the shape can evolve (multi-installment plans,
49
+ * grace-period overrides per pax band, etc.) without a migration
50
+ * for every change.
51
+ *
52
+ * `null` means "no policy configured yet — fall through to
53
+ * hard-coded `noDepositPolicy` (100% upfront)".
54
+ */
55
+ customerPaymentPolicy: jsonb("customer_payment_policy"),
56
+ // Columns retained for migrations from pre-booking-tax-settings
57
+ // beta databases. Runtime code reads/writes `booking_tax_settings`.
58
+ taxPriceMode: text("tax_price_mode").notNull().default("inclusive"),
59
+ taxPolicyProfileId: text("tax_policy_profile_id"),
60
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
61
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
62
+ });
63
+ /**
64
+ * Single-row operator profile: the legal/trading identity that
65
+ * contracts with the customer. Used by contract variables and public
66
+ * checkout / booking-preview legal blocks.
67
+ */
68
+ export const operatorProfile = pgTable("operator_profile", {
69
+ id: typeId("operator_profile"),
70
+ name: text("name"),
71
+ legalName: text("legal_name"),
72
+ vatId: text("vat_id"),
73
+ registrationNumber: text("registration_number"),
74
+ address: text("address"),
75
+ phone: text("phone"),
76
+ email: text("email"),
77
+ website: text("website"),
78
+ license: text("license"),
79
+ licenseAuthority: text("license_authority"),
80
+ signatoryName: text("signatory_name"),
81
+ signatoryRole: text("signatory_role"),
82
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
83
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
84
+ });
85
+ /**
86
+ * Single-row operator payment instructions for customer-facing
87
+ * collection rails such as bank transfer.
88
+ */
89
+ export const operatorPaymentInstructions = pgTable("operator_payment_instructions", {
90
+ id: typeId("operator_payment_instructions"),
91
+ bankTransferBeneficiary: text("bank_transfer_beneficiary"),
92
+ iban: text("iban"),
93
+ bank: text("bank"),
94
+ notes: text("notes"),
95
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
96
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
97
+ });
98
+ /**
99
+ * Single-row operator-level customer payment default. The booking
100
+ * payment-policy cascade uses this only when supplier/category/listing
101
+ * and booking-level policies do not override it.
102
+ */
103
+ export const operatorPaymentDefaults = pgTable("operator_payment_defaults", {
104
+ id: typeId("operator_payment_defaults"),
105
+ customerPaymentPolicy: jsonb("customer_payment_policy"),
106
+ bookingCheckoutUrlTemplate: text("booking_checkout_url_template"),
107
+ invoicePayUrlTemplate: text("invoice_pay_url_template"),
108
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
109
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
110
+ });
111
+ /**
112
+ * Single-row booking tax configuration for booking-create previews,
113
+ * quote recomputation, and booking item tax-line materialization.
114
+ *
115
+ * Kept separate from operator identity and payment instructions:
116
+ * these fields are finance/booking tax policy knobs.
117
+ */
118
+ export const bookingTaxSettings = pgTable("booking_tax_settings", {
119
+ id: typeId("booking_tax_settings"),
120
+ taxPriceMode: text("tax_price_mode").notNull().default("inclusive"),
121
+ taxPolicyProfileId: text("tax_policy_profile_id"),
122
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
123
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
124
+ });
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Operator settings data access — readers/writers + validation for the
3
+ * operator profile, payment instructions/defaults, and booking-tax settings.
4
+ *
5
+ * Transport-agnostic (no Hono): a deployment mounts the HTTP routes over these
6
+ * and injects the readers into the standard modules that need them (legal
7
+ * contract variables, quotes proposal, commerce checkout tax, finance
8
+ * booking-tax). The schema lives in `./schema`.
9
+ */
10
+ import type { BookingTaxSettings, PaymentPolicy } from "@voyant-travel/finance";
11
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
12
+ import { z } from "zod";
13
+ import { operatorPaymentDefaults, operatorPaymentInstructions, operatorProfile } from "./schema.js";
14
+ export declare const updateOperatorProfileSchema: z.ZodObject<{
15
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
16
+ legalName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
17
+ vatId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
18
+ registrationNumber: z.ZodOptional<z.ZodNullable<z.ZodString>>;
19
+ address: z.ZodOptional<z.ZodNullable<z.ZodString>>;
20
+ phone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
21
+ email: z.ZodUnion<[z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodLiteral<"">]>;
22
+ website: z.ZodUnion<[z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodLiteral<"">]>;
23
+ license: z.ZodOptional<z.ZodNullable<z.ZodString>>;
24
+ licenseAuthority: z.ZodOptional<z.ZodNullable<z.ZodString>>;
25
+ signatoryName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
26
+ signatoryRole: z.ZodOptional<z.ZodNullable<z.ZodString>>;
27
+ }, z.core.$strip>;
28
+ export declare const updateOperatorPaymentInstructionsSchema: z.ZodObject<{
29
+ bankTransferBeneficiary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
+ iban: z.ZodOptional<z.ZodNullable<z.ZodString>>;
31
+ bank: z.ZodOptional<z.ZodNullable<z.ZodString>>;
32
+ notes: z.ZodOptional<z.ZodNullable<z.ZodString>>;
33
+ }, z.core.$strip>;
34
+ export declare const updateOperatorPaymentDefaultsSchema: z.ZodObject<{
35
+ customerPaymentPolicy: z.ZodOptional<z.ZodNullable<z.ZodObject<{
36
+ deposit: z.ZodObject<{
37
+ kind: z.ZodEnum<{
38
+ none: "none";
39
+ percent: "percent";
40
+ fixed_cents: "fixed_cents";
41
+ }>;
42
+ percent: z.ZodOptional<z.ZodNumber>;
43
+ amountCents: z.ZodOptional<z.ZodNumber>;
44
+ }, z.core.$strip>;
45
+ minDaysBeforeDepartureForDeposit: z.ZodNumber;
46
+ balanceDueDaysBeforeDeparture: z.ZodNumber;
47
+ balanceDueMinDaysFromNow: z.ZodNumber;
48
+ }, z.core.$strip>>>;
49
+ bookingCheckoutUrlTemplate: z.ZodOptional<z.ZodNullable<z.ZodString>>;
50
+ invoicePayUrlTemplate: z.ZodOptional<z.ZodNullable<z.ZodString>>;
51
+ }, z.core.$strip>;
52
+ export declare const updateOperatorSettingsSchema: z.ZodObject<{
53
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
54
+ legalName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
55
+ vatId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
56
+ registrationNumber: z.ZodOptional<z.ZodNullable<z.ZodString>>;
57
+ address: z.ZodOptional<z.ZodNullable<z.ZodString>>;
58
+ phone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
59
+ email: z.ZodUnion<[z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodLiteral<"">]>;
60
+ website: z.ZodUnion<[z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodLiteral<"">]>;
61
+ license: z.ZodOptional<z.ZodNullable<z.ZodString>>;
62
+ licenseAuthority: z.ZodOptional<z.ZodNullable<z.ZodString>>;
63
+ signatoryName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
64
+ signatoryRole: z.ZodOptional<z.ZodNullable<z.ZodString>>;
65
+ bankTransferBeneficiary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
66
+ iban: z.ZodOptional<z.ZodNullable<z.ZodString>>;
67
+ bank: z.ZodOptional<z.ZodNullable<z.ZodString>>;
68
+ notes: z.ZodOptional<z.ZodNullable<z.ZodString>>;
69
+ customerPaymentPolicy: z.ZodOptional<z.ZodNullable<z.ZodObject<{
70
+ deposit: z.ZodObject<{
71
+ kind: z.ZodEnum<{
72
+ none: "none";
73
+ percent: "percent";
74
+ fixed_cents: "fixed_cents";
75
+ }>;
76
+ percent: z.ZodOptional<z.ZodNumber>;
77
+ amountCents: z.ZodOptional<z.ZodNumber>;
78
+ }, z.core.$strip>;
79
+ minDaysBeforeDepartureForDeposit: z.ZodNumber;
80
+ balanceDueDaysBeforeDeparture: z.ZodNumber;
81
+ balanceDueMinDaysFromNow: z.ZodNumber;
82
+ }, z.core.$strip>>>;
83
+ bookingCheckoutUrlTemplate: z.ZodOptional<z.ZodNullable<z.ZodString>>;
84
+ invoicePayUrlTemplate: z.ZodOptional<z.ZodNullable<z.ZodString>>;
85
+ }, z.core.$strip>;
86
+ export type UpdateOperatorProfileInput = z.infer<typeof updateOperatorProfileSchema>;
87
+ export type UpdateOperatorPaymentInstructionsInput = z.infer<typeof updateOperatorPaymentInstructionsSchema>;
88
+ export type UpdateOperatorPaymentDefaultsInput = z.infer<typeof updateOperatorPaymentDefaultsSchema>;
89
+ export type UpdateOperatorSettingsInput = z.infer<typeof updateOperatorSettingsSchema>;
90
+ type OperatorProfileRow = typeof operatorProfile.$inferSelect;
91
+ type OperatorPaymentInstructionsRow = typeof operatorPaymentInstructions.$inferSelect;
92
+ type OperatorPaymentDefaultsRow = typeof operatorPaymentDefaults.$inferSelect;
93
+ export declare function getOperatorProfile(db: PostgresJsDatabase): Promise<{
94
+ id: string;
95
+ name: string | null;
96
+ legalName: string | null;
97
+ vatId: string | null;
98
+ registrationNumber: string | null;
99
+ address: string | null;
100
+ phone: string | null;
101
+ email: string | null;
102
+ website: string | null;
103
+ license: string | null;
104
+ licenseAuthority: string | null;
105
+ signatoryName: string | null;
106
+ signatoryRole: string | null;
107
+ createdAt: Date;
108
+ updatedAt: Date;
109
+ } | null>;
110
+ export declare function upsertOperatorProfile(db: PostgresJsDatabase, patch: UpdateOperatorProfileInput): Promise<{
111
+ id: string;
112
+ name: string | null;
113
+ createdAt: Date;
114
+ updatedAt: Date;
115
+ email: string | null;
116
+ phone: string | null;
117
+ vatId: string | null;
118
+ address: string | null;
119
+ legalName: string | null;
120
+ registrationNumber: string | null;
121
+ website: string | null;
122
+ license: string | null;
123
+ licenseAuthority: string | null;
124
+ signatoryName: string | null;
125
+ signatoryRole: string | null;
126
+ } | null>;
127
+ export declare function getOperatorPaymentInstructions(db: PostgresJsDatabase): Promise<{
128
+ id: string;
129
+ bankTransferBeneficiary: string | null;
130
+ iban: string | null;
131
+ bank: string | null;
132
+ notes: string | null;
133
+ createdAt: Date;
134
+ updatedAt: Date;
135
+ } | null>;
136
+ export declare function upsertOperatorPaymentInstructions(db: PostgresJsDatabase, patch: UpdateOperatorPaymentInstructionsInput): Promise<{
137
+ id: string;
138
+ createdAt: Date;
139
+ updatedAt: Date;
140
+ notes: string | null;
141
+ iban: string | null;
142
+ bankTransferBeneficiary: string | null;
143
+ bank: string | null;
144
+ } | null>;
145
+ export declare function getOperatorPaymentDefaults(db: PostgresJsDatabase): Promise<{
146
+ id: string;
147
+ customerPaymentPolicy: unknown;
148
+ bookingCheckoutUrlTemplate: string | null;
149
+ invoicePayUrlTemplate: string | null;
150
+ createdAt: Date;
151
+ updatedAt: Date;
152
+ } | null>;
153
+ export declare function upsertOperatorPaymentDefaults(db: PostgresJsDatabase, patch: UpdateOperatorPaymentDefaultsInput): Promise<{
154
+ id: string;
155
+ customerPaymentPolicy: unknown;
156
+ createdAt: Date;
157
+ updatedAt: Date;
158
+ bookingCheckoutUrlTemplate: string | null;
159
+ invoicePayUrlTemplate: string | null;
160
+ } | null>;
161
+ export declare function resolveOperatorDefaultPaymentPolicy(db: PostgresJsDatabase): Promise<PaymentPolicy | null>;
162
+ export declare function resolveBookingTaxSettings(db: PostgresJsDatabase): Promise<BookingTaxSettings>;
163
+ export declare function updateBookingTaxSettings(db: PostgresJsDatabase, patch: BookingTaxSettings): Promise<BookingTaxSettings>;
164
+ export declare function toPublicOperatorProfile(row: OperatorProfileRow, defaults?: OperatorPaymentDefaultsRow | null): PublicOperatorProfile;
165
+ export interface PublicOperatorProfile {
166
+ name: string;
167
+ legalName: string;
168
+ address: string;
169
+ phone: string;
170
+ email: string;
171
+ website: string;
172
+ license: string;
173
+ licenseAuthority: string;
174
+ customerPaymentPolicy: PaymentPolicy | null;
175
+ bookingCheckoutUrlTemplate: string | null;
176
+ invoicePayUrlTemplate: string | null;
177
+ }
178
+ type CombinedOperatorSettings = Partial<OperatorProfileRow> & Partial<OperatorPaymentInstructionsRow> & {
179
+ customerPaymentPolicy?: unknown;
180
+ bookingCheckoutUrlTemplate?: string | null;
181
+ invoicePayUrlTemplate?: string | null;
182
+ };
183
+ export declare function getOperatorSettings(db: PostgresJsDatabase): Promise<CombinedOperatorSettings | null>;
184
+ export declare function upsertOperatorSettings(db: PostgresJsDatabase, patch: UpdateOperatorSettingsInput): Promise<CombinedOperatorSettings | null>;
185
+ export declare function toPublicOperatorSettings(row: Awaited<ReturnType<typeof getOperatorSettings>>): PublicOperatorProfile;
186
+ export {};
187
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Operator settings data access — readers/writers + validation for the
3
+ * operator profile, payment instructions/defaults, and booking-tax settings.
4
+ *
5
+ * Transport-agnostic (no Hono): a deployment mounts the HTTP routes over these
6
+ * and injects the readers into the standard modules that need them (legal
7
+ * contract variables, quotes proposal, commerce checkout tax, finance
8
+ * booking-tax). The schema lives in `./schema`.
9
+ */
10
+ import { desc, eq } from "drizzle-orm";
11
+ import { z } from "zod";
12
+ import { bookingTaxSettings, operatorPaymentDefaults, operatorPaymentInstructions, operatorProfile, } from "./schema.js";
13
+ const depositRuleSchema = z.object({
14
+ kind: z.enum(["none", "percent", "fixed_cents"]),
15
+ percent: z.number().min(0).max(100).optional(),
16
+ amountCents: z.number().int().min(0).optional(),
17
+ });
18
+ const paymentPolicySchema = z.object({
19
+ deposit: depositRuleSchema,
20
+ minDaysBeforeDepartureForDeposit: z.number().int().min(0),
21
+ balanceDueDaysBeforeDeparture: z.number().int().min(0),
22
+ balanceDueMinDaysFromNow: z.number().int().min(0),
23
+ });
24
+ export const updateOperatorProfileSchema = z.object({
25
+ name: z.string().nullable().optional(),
26
+ legalName: z.string().nullable().optional(),
27
+ vatId: z.string().nullable().optional(),
28
+ registrationNumber: z.string().nullable().optional(),
29
+ address: z.string().nullable().optional(),
30
+ phone: z.string().nullable().optional(),
31
+ email: z.string().email().nullable().optional().or(z.literal("")),
32
+ website: z.string().url().nullable().optional().or(z.literal("")),
33
+ license: z.string().nullable().optional(),
34
+ licenseAuthority: z.string().nullable().optional(),
35
+ signatoryName: z.string().nullable().optional(),
36
+ signatoryRole: z.string().nullable().optional(),
37
+ });
38
+ export const updateOperatorPaymentInstructionsSchema = z.object({
39
+ bankTransferBeneficiary: z.string().nullable().optional(),
40
+ iban: z.string().nullable().optional(),
41
+ bank: z.string().nullable().optional(),
42
+ notes: z.string().nullable().optional(),
43
+ });
44
+ export const updateOperatorPaymentDefaultsSchema = z.object({
45
+ customerPaymentPolicy: paymentPolicySchema.nullable().optional(),
46
+ bookingCheckoutUrlTemplate: z.string().trim().nullable().optional(),
47
+ invoicePayUrlTemplate: z.string().trim().nullable().optional(),
48
+ });
49
+ export const updateOperatorSettingsSchema = updateOperatorProfileSchema
50
+ .merge(updateOperatorPaymentInstructionsSchema)
51
+ .merge(updateOperatorPaymentDefaultsSchema);
52
+ export async function getOperatorProfile(db) {
53
+ const [row] = await db
54
+ .select()
55
+ .from(operatorProfile)
56
+ .orderBy(desc(operatorProfile.createdAt))
57
+ .limit(1);
58
+ return row ?? null;
59
+ }
60
+ export async function upsertOperatorProfile(db, patch) {
61
+ const existing = await getOperatorProfile(db);
62
+ if (!existing) {
63
+ const [created] = await db.insert(operatorProfile).values(patch).returning();
64
+ return created ?? null;
65
+ }
66
+ const [updated] = await db
67
+ .update(operatorProfile)
68
+ .set({ ...patch, updatedAt: new Date() })
69
+ .where(eq(operatorProfile.id, existing.id))
70
+ .returning();
71
+ return updated ?? null;
72
+ }
73
+ export async function getOperatorPaymentInstructions(db) {
74
+ const [row] = await db
75
+ .select()
76
+ .from(operatorPaymentInstructions)
77
+ .orderBy(desc(operatorPaymentInstructions.createdAt))
78
+ .limit(1);
79
+ return row ?? null;
80
+ }
81
+ export async function upsertOperatorPaymentInstructions(db, patch) {
82
+ const existing = await getOperatorPaymentInstructions(db);
83
+ if (!existing) {
84
+ const [created] = await db.insert(operatorPaymentInstructions).values(patch).returning();
85
+ return created ?? null;
86
+ }
87
+ const [updated] = await db
88
+ .update(operatorPaymentInstructions)
89
+ .set({ ...patch, updatedAt: new Date() })
90
+ .where(eq(operatorPaymentInstructions.id, existing.id))
91
+ .returning();
92
+ return updated ?? null;
93
+ }
94
+ export async function getOperatorPaymentDefaults(db) {
95
+ const [row] = await db
96
+ .select()
97
+ .from(operatorPaymentDefaults)
98
+ .orderBy(desc(operatorPaymentDefaults.createdAt))
99
+ .limit(1);
100
+ return row ?? null;
101
+ }
102
+ export async function upsertOperatorPaymentDefaults(db, patch) {
103
+ const existing = await getOperatorPaymentDefaults(db);
104
+ const values = {};
105
+ if ("customerPaymentPolicy" in patch) {
106
+ values.customerPaymentPolicy = (patch.customerPaymentPolicy ?? null);
107
+ }
108
+ if ("bookingCheckoutUrlTemplate" in patch) {
109
+ values.bookingCheckoutUrlTemplate = patch.bookingCheckoutUrlTemplate?.trim() || null;
110
+ }
111
+ if ("invoicePayUrlTemplate" in patch) {
112
+ values.invoicePayUrlTemplate = patch.invoicePayUrlTemplate?.trim() || null;
113
+ }
114
+ if (!existing) {
115
+ const [created] = await db.insert(operatorPaymentDefaults).values(values).returning();
116
+ return created ?? null;
117
+ }
118
+ if (Object.keys(values).length === 0)
119
+ return existing;
120
+ const [updated] = await db
121
+ .update(operatorPaymentDefaults)
122
+ .set({
123
+ ...values,
124
+ updatedAt: new Date(),
125
+ })
126
+ .where(eq(operatorPaymentDefaults.id, existing.id))
127
+ .returning();
128
+ return updated ?? null;
129
+ }
130
+ export async function resolveOperatorDefaultPaymentPolicy(db) {
131
+ const defaults = await getOperatorPaymentDefaults(db);
132
+ return defaults?.customerPaymentPolicy ?? null;
133
+ }
134
+ export async function resolveBookingTaxSettings(db) {
135
+ const [settings] = await db
136
+ .select()
137
+ .from(bookingTaxSettings)
138
+ .orderBy(desc(bookingTaxSettings.createdAt))
139
+ .limit(1);
140
+ return {
141
+ taxPriceMode: settings?.taxPriceMode === "exclusive" ? "exclusive" : "inclusive",
142
+ taxPolicyProfileId: settings?.taxPolicyProfileId ?? null,
143
+ };
144
+ }
145
+ export async function updateBookingTaxSettings(db, patch) {
146
+ const [existing] = await db
147
+ .select()
148
+ .from(bookingTaxSettings)
149
+ .orderBy(desc(bookingTaxSettings.createdAt))
150
+ .limit(1);
151
+ if (!existing) {
152
+ const [created] = await db
153
+ .insert(bookingTaxSettings)
154
+ .values({
155
+ taxPriceMode: patch.taxPriceMode === "exclusive" ? "exclusive" : "inclusive",
156
+ taxPolicyProfileId: patch.taxPolicyProfileId ?? null,
157
+ })
158
+ .returning();
159
+ return {
160
+ taxPriceMode: created?.taxPriceMode === "exclusive" ? "exclusive" : "inclusive",
161
+ taxPolicyProfileId: created?.taxPolicyProfileId ?? null,
162
+ };
163
+ }
164
+ const [updated] = await db
165
+ .update(bookingTaxSettings)
166
+ .set({
167
+ taxPriceMode: patch.taxPriceMode === "exclusive" ? "exclusive" : "inclusive",
168
+ taxPolicyProfileId: patch.taxPolicyProfileId ?? null,
169
+ updatedAt: new Date(),
170
+ })
171
+ .where(eq(bookingTaxSettings.id, existing.id))
172
+ .returning();
173
+ return {
174
+ taxPriceMode: updated?.taxPriceMode === "exclusive" ? "exclusive" : "inclusive",
175
+ taxPolicyProfileId: updated?.taxPolicyProfileId ?? null,
176
+ };
177
+ }
178
+ export function toPublicOperatorProfile(row, defaults) {
179
+ return {
180
+ name: row.name ?? "",
181
+ legalName: row.legalName ?? "",
182
+ address: row.address ?? "",
183
+ phone: row.phone ?? "",
184
+ email: row.email ?? "",
185
+ website: row.website ?? "",
186
+ license: row.license ?? "",
187
+ licenseAuthority: row.licenseAuthority ?? "",
188
+ customerPaymentPolicy: defaults?.customerPaymentPolicy ?? null,
189
+ bookingCheckoutUrlTemplate: defaults?.bookingCheckoutUrlTemplate ?? null,
190
+ invoicePayUrlTemplate: defaults?.invoicePayUrlTemplate ?? null,
191
+ };
192
+ }
193
+ function combineOperatorSettings(profile, instructions, defaults) {
194
+ if (!profile && !instructions && !defaults)
195
+ return null;
196
+ return {
197
+ ...(profile ?? {}),
198
+ bankTransferBeneficiary: instructions?.bankTransferBeneficiary ?? null,
199
+ iban: instructions?.iban ?? null,
200
+ bank: instructions?.bank ?? null,
201
+ notes: instructions?.notes ?? null,
202
+ customerPaymentPolicy: defaults?.customerPaymentPolicy ?? null,
203
+ bookingCheckoutUrlTemplate: defaults?.bookingCheckoutUrlTemplate ?? null,
204
+ invoicePayUrlTemplate: defaults?.invoicePayUrlTemplate ?? null,
205
+ };
206
+ }
207
+ export async function getOperatorSettings(db) {
208
+ const [profile, instructions, defaults] = await Promise.all([
209
+ getOperatorProfile(db),
210
+ getOperatorPaymentInstructions(db),
211
+ getOperatorPaymentDefaults(db),
212
+ ]);
213
+ return combineOperatorSettings(profile, instructions, defaults);
214
+ }
215
+ export async function upsertOperatorSettings(db, patch) {
216
+ const paymentDefaultsPatch = {};
217
+ if ("customerPaymentPolicy" in patch) {
218
+ paymentDefaultsPatch.customerPaymentPolicy = patch.customerPaymentPolicy;
219
+ }
220
+ if ("bookingCheckoutUrlTemplate" in patch) {
221
+ paymentDefaultsPatch.bookingCheckoutUrlTemplate = patch.bookingCheckoutUrlTemplate;
222
+ }
223
+ if ("invoicePayUrlTemplate" in patch) {
224
+ paymentDefaultsPatch.invoicePayUrlTemplate = patch.invoicePayUrlTemplate;
225
+ }
226
+ const [profile, instructions, defaults] = await Promise.all([
227
+ upsertOperatorProfile(db, {
228
+ name: patch.name,
229
+ legalName: patch.legalName,
230
+ vatId: patch.vatId,
231
+ registrationNumber: patch.registrationNumber,
232
+ address: patch.address,
233
+ phone: patch.phone,
234
+ email: patch.email,
235
+ website: patch.website,
236
+ license: patch.license,
237
+ licenseAuthority: patch.licenseAuthority,
238
+ signatoryName: patch.signatoryName,
239
+ signatoryRole: patch.signatoryRole,
240
+ }),
241
+ upsertOperatorPaymentInstructions(db, {
242
+ bankTransferBeneficiary: patch.bankTransferBeneficiary,
243
+ iban: patch.iban,
244
+ bank: patch.bank,
245
+ notes: patch.notes,
246
+ }),
247
+ upsertOperatorPaymentDefaults(db, paymentDefaultsPatch),
248
+ ]);
249
+ return combineOperatorSettings(profile, instructions, defaults);
250
+ }
251
+ export function toPublicOperatorSettings(row) {
252
+ return {
253
+ name: row?.name ?? "",
254
+ legalName: row?.legalName ?? "",
255
+ address: row?.address ?? "",
256
+ phone: row?.phone ?? "",
257
+ email: row?.email ?? "",
258
+ website: row?.website ?? "",
259
+ license: row?.license ?? "",
260
+ licenseAuthority: row?.licenseAuthority ?? "",
261
+ customerPaymentPolicy: row?.customerPaymentPolicy ?? null,
262
+ bookingCheckoutUrlTemplate: row?.bookingCheckoutUrlTemplate ?? null,
263
+ invoicePayUrlTemplate: row?.invoicePayUrlTemplate ?? null,
264
+ };
265
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.test.d.ts","sourceRoot":"","sources":["../src/service.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { toPublicOperatorProfile, updateOperatorPaymentDefaultsSchema, updateOperatorProfileSchema, } from "./service.js";
3
+ // Minimal row shape the DTO mapper reads (the full row also has id/timestamps).
4
+ function profileRow(overrides = {}) {
5
+ return {
6
+ id: "oppf_test",
7
+ name: "Acme Tours",
8
+ legalName: "Acme Tours SRL",
9
+ vatId: null,
10
+ registrationNumber: null,
11
+ address: "1 Main St",
12
+ phone: "+40 700 000 000",
13
+ email: "hello@acme.test",
14
+ website: "https://acme.test",
15
+ license: "ANPC-123",
16
+ licenseAuthority: "ANPC",
17
+ signatoryName: null,
18
+ signatoryRole: null,
19
+ createdAt: new Date(0),
20
+ updatedAt: new Date(0),
21
+ ...overrides,
22
+ };
23
+ }
24
+ describe("toPublicOperatorProfile", () => {
25
+ it("maps profile + payment defaults into the public DTO", () => {
26
+ const dto = toPublicOperatorProfile(profileRow(), {
27
+ id: "opdp_test",
28
+ customerPaymentPolicy: { deposit: { kind: "none" } },
29
+ bookingCheckoutUrlTemplate: "https://pay.acme.test/{booking}",
30
+ invoicePayUrlTemplate: null,
31
+ createdAt: new Date(0),
32
+ updatedAt: new Date(0),
33
+ });
34
+ expect(dto.name).toBe("Acme Tours");
35
+ expect(dto.email).toBe("hello@acme.test");
36
+ expect(dto.bookingCheckoutUrlTemplate).toBe("https://pay.acme.test/{booking}");
37
+ expect(dto.invoicePayUrlTemplate).toBeNull();
38
+ expect(dto.customerPaymentPolicy).toEqual({ deposit: { kind: "none" } });
39
+ });
40
+ it("coalesces missing identity fields to empty strings and policy to null", () => {
41
+ const dto = toPublicOperatorProfile(profileRow({ name: null, email: null, website: null, license: null }));
42
+ expect(dto.name).toBe("");
43
+ expect(dto.email).toBe("");
44
+ expect(dto.website).toBe("");
45
+ expect(dto.license).toBe("");
46
+ expect(dto.customerPaymentPolicy).toBeNull();
47
+ expect(dto.bookingCheckoutUrlTemplate).toBeNull();
48
+ });
49
+ });
50
+ describe("validation schemas", () => {
51
+ it("accepts a valid profile patch and an empty email/website", () => {
52
+ expect(updateOperatorProfileSchema.safeParse({ name: "X", email: "" }).success).toBe(true);
53
+ expect(updateOperatorProfileSchema.safeParse({ email: "a@b.test" }).success).toBe(true);
54
+ });
55
+ it("rejects a malformed email", () => {
56
+ expect(updateOperatorProfileSchema.safeParse({ email: "not-an-email" }).success).toBe(false);
57
+ });
58
+ it("validates the customer payment policy shape", () => {
59
+ expect(updateOperatorPaymentDefaultsSchema.safeParse({
60
+ customerPaymentPolicy: {
61
+ deposit: { kind: "percent", percent: 20 },
62
+ minDaysBeforeDepartureForDeposit: 0,
63
+ balanceDueDaysBeforeDeparture: 30,
64
+ balanceDueMinDaysFromNow: 7,
65
+ },
66
+ }).success).toBe(true);
67
+ expect(updateOperatorPaymentDefaultsSchema.safeParse({
68
+ customerPaymentPolicy: { deposit: { kind: "bogus" } },
69
+ }).success).toBe(false);
70
+ });
71
+ });