feat(interests): CM-2 Part B — interest_berths price override (data + resolver)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
-- CM-2 Part B: per-interest, per-berth deal-price override.
|
||||
-- Null = use the berth's canonical list price (berths.price). When set, this
|
||||
-- supersedes the list price for THIS interest's generated documents
|
||||
-- (resolved in eoi-context via resolveBerthPriceForInterest).
|
||||
ALTER TABLE interest_berths
|
||||
ADD COLUMN IF NOT EXISTS price_override numeric,
|
||||
ADD COLUMN IF NOT EXISTS price_override_currency text;
|
||||
@@ -165,6 +165,10 @@ export const interestBerths = pgTable(
|
||||
addedBy: text('added_by'),
|
||||
addedAt: timestamp('added_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
notes: text('notes'),
|
||||
// CM-2 Part B: deal-specific price for THIS (interest, berth). Null = use
|
||||
// the berth's canonical list price. Does not touch berths.price.
|
||||
priceOverride: numeric('price_override'),
|
||||
priceOverrideCurrency: text('price_override_currency'),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('idx_ib_interest_berth').on(table.interestId, table.berthId),
|
||||
|
||||
@@ -170,6 +170,8 @@ export async function listBerthsForInterest(
|
||||
addedBy: interestBerths.addedBy,
|
||||
addedAt: interestBerths.addedAt,
|
||||
notes: interestBerths.notes,
|
||||
priceOverride: interestBerths.priceOverride,
|
||||
priceOverrideCurrency: interestBerths.priceOverrideCurrency,
|
||||
mooringNumber: berths.mooringNumber,
|
||||
area: berths.area,
|
||||
status: berths.status,
|
||||
@@ -444,3 +446,49 @@ export async function removeInterestBerth(
|
||||
.delete(interestBerths)
|
||||
.where(and(eq(interestBerths.interestId, interestId), eq(interestBerths.berthId, berthId)));
|
||||
}
|
||||
|
||||
// ─── Per-interest price override (CM-2 Part B) ───────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve the effective price for a berth in the context of an interest. The
|
||||
* deal-specific override (when set) supersedes the berth's canonical list
|
||||
* price; the override carries its own currency, falling back to the base
|
||||
* currency when null. Pure — safe to unit-test without a DB.
|
||||
*/
|
||||
export function resolveBerthPriceForInterest(
|
||||
override: { priceOverride: string | null; priceOverrideCurrency: string | null },
|
||||
base: { price: string | null; priceCurrency: string },
|
||||
): { price: string | null; currency: string } {
|
||||
if (override.priceOverride != null) {
|
||||
return {
|
||||
price: override.priceOverride,
|
||||
currency: override.priceOverrideCurrency ?? base.priceCurrency,
|
||||
};
|
||||
}
|
||||
return { price: base.price, currency: base.priceCurrency };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set (or clear, when `price` is null) the deal-specific price for one
|
||||
* (interest, berth). Tenant-scoped: the interest must belong to `portId`.
|
||||
* Does not touch `berths.price`.
|
||||
*/
|
||||
export async function setBerthPriceOverride(
|
||||
interestId: string,
|
||||
berthId: string,
|
||||
price: number | null,
|
||||
currency: string | null,
|
||||
portId: string,
|
||||
): Promise<void> {
|
||||
const interestRow = await db.query.interests.findFirst({
|
||||
where: and(eq(interests.id, interestId), eq(interests.portId, portId)),
|
||||
});
|
||||
if (!interestRow) throw new NotFoundError('Interest');
|
||||
await db
|
||||
.update(interestBerths)
|
||||
.set({
|
||||
priceOverride: price == null ? null : String(price),
|
||||
priceOverrideCurrency: price == null ? null : (currency ?? 'USD'),
|
||||
})
|
||||
.where(and(eq(interestBerths.interestId, interestId), eq(interestBerths.berthId, berthId)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user