Broker Portal Offer Generator Pricing Alignment Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make broker portal quotes and broker-created orders calculate prices exactly like the offer generator behind pages/calculator.js.
Architecture: The offer generator logic lives in hooks/useCalculatorUpdater.js, not in pages/calculator.js. Add a small pure broker pricing helper that mirrors createCalculateVehicle() and the initial recalculateVehicles() behavior: days are clamped to at least 1, vehicle tariffs take priority, vehicle base values are the fallback, vehicle seasons are applied, total price is rounded before delivery is added, and included kilometers are derived from baseKmDay * days.
Tech Stack: Next.js API routes, Fauna FQL, Flow/TypeScript hybrid codebase, Vitest, Elysia broker portal API.
Verified Offer Generator Logic
The entry page is pages/calculator.js, but it only renders containers/Calculator.
The pricing behavior to mirror is in hooks/useCalculatorUpdater.js:
calculateResults()copies selected vehicles into local calculated vehicles:hooks/useCalculatorUpdater.js:496-530.createCalculateVehicle()calculates the initial offer row:hooks/useCalculatorUpdater.js:408-493.recalculateVehicles()applies calculator modifiers after rows are created:hooks/useCalculatorUpdater.js:52-155.
Initial offer row logic:
- Read
tarifficationVariantandstandardBufferTimefrom settings. - Normalize tariffication through
getValidTarifficationVariant(). - Calculate
days = Math.max(getDaysAmountByISO(...), 1). - Get price fields through
getDefaultVehicleParameters(store, vehicle, days). - Apply vehicle seasons through
getSeasonsTariffsMatrix(null, vehicle)andcalculatePriceWithSeasons(...). - If there is no season matrix, use
price * days. - Set calculated total price to
roundToHundreds(totalBasePrice) + deliveryPrice. - Set
extraKmPriceto tariff/base extra km price. - Set
baseKmDaytoInfinitywhen unlimited, otherwise daily base km. - Set
includedtoMath.round(baseKmDay * days).
Important: offer generator does not use settings.daysTariffsMatrix as a runtime fallback. Any matrix effects reach the offer generator only if they have already been materialized into vehicle.tariffs.
Current Broker Gap
apps/broker-portal does not calculate quote prices directly. It calls createPowerRentClient().getBookingQuote(), implemented by apps/broker-portal/src/lib/power-rent-client.http.ts, which posts to /api/public/broker/availability.
Current broker quote path:
apps/broker-portal/src/api/routes/vehicles.ts:127-132apps/broker-portal/src/lib/power-rent-client.http.ts:82-110pages/api/public/broker/availability.ts:96-119
Current broker order path:
apps/broker-portal/src/lib/power-rent-client.http.ts:117-145pages/api/public/broker/create-order.ts:221-227pages/api/public/broker/create-order.ts:243-276
The broker quote route currently uses calculateTotalPricePure(...). That is close, but not exactly the offer generator:
- It does not explicitly clamp days to at least 1 in the broker helper layer.
- It returns
pricing.totalPrice, which includes parts the offer generator initial row does not use for quote display. - It does not return
included, while offer generator computes included km asMath.round(baseKmDay * days). - Broker order creation uses
composeOrderInput(...), which can diverge from the broker quote if the quote logic is changed but order input still uses raw vehicle data.
Target Behavior
Broker quote and broker order creation should use one shared pure helper that mirrors offer generator initial pricing:
days = Math.max(getDaysAmountByISO({ startDate, finishDate, tarifficationVariant, standardBufferTime }), 1)defaults = getDefaultVehicleParametersPure({ vehicleTariffs, vehiclePrice, extraKmPrice, vehicleBaseKmDay, days })totalBasePrice = seasons ? calculatePriceWithSeasons(...) : defaults.price * daystotalPrice = roundToHundreds(totalBasePrice) + deliveryPricebaseKmDay = defaults.unlimitedKilometers ? Infinity : defaults.baseKmDayincluded = Math.round(baseKmDay * days)The helper must not read or apply settings.daysTariffsMatrix directly.
Files
- Create:
pages/api/public/broker/offer-generator-pricing.ts- Pure implementation of the offer generator initial row calculation for broker server routes.
- Modify:
pages/api/public/broker/availability.ts- Replace direct
calculateTotalPricePure(...)quote calculation with the offer-generator-compatible helper. - Include
includedin available vehicle quote responses.
- Replace direct
- Modify:
pages/api/public/broker/create-order.ts- Use the same helper for quote-equivalent values before composing the order.
- Pass helper-derived
extraKmPriceand a pricing-normalized vehicle object intocomposeOrderInput(...)so order creation remains consistent with quote pricing.
- Modify:
_tests_/pages/api/public/broker/availability.test.ts- Cover offer generator pricing semantics for broker availability.
- Modify:
_tests_/pages/api/public/broker/create-order.test.ts- Cover broker order composition uses offer-generator-compatible values.
- Modify:
apps/broker-portal/src/api/__tests__/vehicle-routes.test.ts- Assert broker portal passes through the aligned quote fields.
- Optional modify:
@power-rent/broker-typessource ifVehicleAvailability/BookingQuoteResulttypes do not includeincluded.
Task 1: Add Offer Generator Pricing Helper
Files:
-
Create:
pages/api/public/broker/offer-generator-pricing.ts -
Step 1: Create pure helper
Create pages/api/public/broker/offer-generator-pricing.ts:
// @ts-expect-error Flow module consumed from TS API routesimport { calculatePriceWithSeasons, getDaysAmountByISO, getDefaultVehicleParametersPure, getNumber, roundNumber,} from '@power-rent/lib/calculators';// @ts-expect-error Flow module consumed from TS API routesimport { getValidTarifficationVariant } from '@power-rent/lib/validators';
const roundToHundreds = roundNumber(2);
type BrokerVehicleTariff = Record<string, unknown>;
type BrokerSeasonTariffMatrix = { from?: string | null; to?: string | null; percents?: number | null; name?: string | null;};
type BrokerOfferGeneratorPricingArgs = { dateFrom: string; dateTo: string; tarifficationVariant?: string | null; standardBufferTime?: number | null; vehicleTariffs?: BrokerVehicleTariff[] | null; vehiclePrice?: number | null; extraKmPrice?: number | null; vehicleBaseKmDay?: number | null; seasonsTariffsMatrix?: BrokerSeasonTariffMatrix[] | null; deliveryPrice?: number | null; roundResults?: boolean | null; tariffPriceRoundingValue?: number | null; tariffRoundPriceUp?: boolean | null;};
type BrokerOfferGeneratorPricingResult = { totalPrice: number; dailyPrice: number; days: number; extraKmPrice: number; baseKmDay: number; included: number; unlimitedKilometers: boolean;};
export function calculateBrokerOfferGeneratorPrice({ dateFrom, dateTo, tarifficationVariant, standardBufferTime, vehicleTariffs, vehiclePrice, extraKmPrice, vehicleBaseKmDay, seasonsTariffsMatrix, deliveryPrice, roundResults, tariffPriceRoundingValue, tariffRoundPriceUp,}: BrokerOfferGeneratorPricingArgs): BrokerOfferGeneratorPricingResult { const validTarifficationVariant = getValidTarifficationVariant(tarifficationVariant);
const days = Math.max(getDaysAmountByISO({ startDate: dateFrom, finishDate: dateTo, tarifficationVariant: validTarifficationVariant, standardBufferTime: getNumber(standardBufferTime), }), 1);
const defaults = getDefaultVehicleParametersPure({ vehicleTariffs: vehicleTariffs || [], vehiclePrice: getNumber(vehiclePrice), extraKmPrice: getNumber(extraKmPrice), vehicleBaseKmDay: getNumber(vehicleBaseKmDay), days, });
const totalBasePrice = seasonsTariffsMatrix?.length ? calculatePriceWithSeasons({ seasonsTariffsMatrix, dateFromISO: dateFrom, dateToISO: dateTo, days, pricePerDay: roundToHundreds(Number(defaults.price)), roundResults: Boolean(roundResults), tariffPriceRoundingValue: getNumber(tariffPriceRoundingValue), tariffRoundPriceUp: Boolean(tariffRoundPriceUp), }) : getNumber(defaults.price) * days;
const totalPrice = roundToHundreds(totalBasePrice) + getNumber(deliveryPrice); const baseKmDay = defaults.unlimitedKilometers ? Infinity : defaults.baseKmDay; const included = Math.round(baseKmDay * days);
return { totalPrice, dailyPrice: defaults.price, days, extraKmPrice: defaults.unlimitedKilometers ? 0 : defaults.extraKmPrice, baseKmDay, included, unlimitedKilometers: defaults.unlimitedKilometers, };}- Step 2: Run no tests yet
The helper is unused until Task 2. Tests are added before wiring it in.
Task 2: Align Broker Availability Quotes
Files:
-
Modify:
pages/api/public/broker/availability.ts -
Modify:
_tests_/pages/api/public/broker/availability.test.ts -
Step 1: Update test mocks
In _tests_/pages/api/public/broker/availability.test.ts, change the hoisted mock block to include mockCalculateOfferGeneratorPrice:
const { mockFaunaQuery, mockFaunaClose, mockCheckAvailability, mockCalculatePrice, mockGetSeasonMatrix, mockCalculateOfferGeneratorPrice,} = vi.hoisted(() => ({ mockFaunaQuery: vi.fn(), mockFaunaClose: vi.fn(), mockCheckAvailability: vi.fn(), mockCalculatePrice: vi.fn(), mockGetSeasonMatrix: vi.fn(), mockCalculateOfferGeneratorPrice: vi.fn(),}));Add this mock after the calculators mock:
vi.mock('../../../../../pages/api/public/broker/offer-generator-pricing', () => ({ calculateBrokerOfferGeneratorPrice: mockCalculateOfferGeneratorPrice,}));In beforeEach, add:
mockCalculateOfferGeneratorPrice.mockReturnValue({ totalPrice: 400, dailyPrice: 100, days: 4, extraKmPrice: 0.25, baseKmDay: 200, included: 800, unlimitedKilometers: false,});- Step 2: Write failing quote test
Append this test after returns availability with pricing for available vehicles:
it('quotes vehicles through the offer generator pricing helper', async () => { const seasonsMatrix = [{ from: '2026-04-01', to: '2026-04-10', percents: 10, name: 'Spring' }]; mockGetSeasonMatrix.mockReturnValue(seasonsMatrix); mockCalculateOfferGeneratorPrice.mockReturnValueOnce({ totalPrice: 450, dailyPrice: 90, days: 5, extraKmPrice: 0.25, baseKmDay: 300, included: 1500, unlimitedKilometers: false, });
mockFaunaQuery .mockResolvedValueOnce({ data: tenantSettings }) .mockResolvedValueOnce({ data: { price: 100, tariffs: [{ type: 'DaysTariff', fromDays: 4, upToDays: 7, price: 90, extraKmPrice: 0.25, baseKmDay: 300, unlimitedKilometers: false }], extraKmPrice: 0.25, baseKmDay: 250, unlimitedKilometers: false, seasonsTariffsMatrix: [], }, });
const { req, res, getStatus, getJson } = createReqRes({ body: { ...validBody, vehicleIds: ['v1'], dateTo: '2026-04-06T08:00:00.000Z' }, });
await handler(req, res);
expect(getStatus()).toBe(200); expect(mockCalculateOfferGeneratorPrice).toHaveBeenCalledWith({ dateFrom: '2026-04-01T08:00:00.000+00:00', dateTo: '2026-04-06T08:00:00.000+00:00', tarifficationVariant: tenantSettings.tarifficationVariant, standardBufferTime: tenantSettings.standardBufferTime, vehicleTariffs: [{ type: 'DaysTariff', fromDays: 4, upToDays: 7, price: 90, extraKmPrice: 0.25, baseKmDay: 300, unlimitedKilometers: false }], vehiclePrice: 100, extraKmPrice: 0.25, vehicleBaseKmDay: 250, seasonsTariffsMatrix: seasonsMatrix, deliveryPrice: 0, roundResults: tenantSettings.roundResults, tariffPriceRoundingValue: tenantSettings.tariffPriceRoundingValue, tariffRoundPriceUp: tenantSettings.tariffRoundPriceUp, });
const json = getJson() as { vehicles: Array<Record<string, unknown>>; }; expect(json.vehicles[0]).toMatchObject({ vehicleId: 'v1', available: true, totalPrice: 450, dailyPrice: 90, days: 5, baseKmDay: 300, included: 1500, });});- Step 3: Run failing test
Run: yarn test _tests_/pages/api/public/broker/availability.test.ts
Expected: FAIL because availability.ts still calls calculateTotalPricePure(...) and does not return included.
- Step 4: Import helper in availability route
In pages/api/public/broker/availability.ts, add:
import { calculateBrokerOfferGeneratorPrice } from './offer-generator-pricing';- Step 5: Add
includedto response type
In BrokerVehicleQuoteAvailable, add:
included: number;- Step 6: Replace direct calculator call
In buildVehicleQuote, replace the const pricing = calculateTotalPricePure({ ... }) block with:
const pricing = calculateBrokerOfferGeneratorPrice({ dateFrom, dateTo, tarifficationVariant, standardBufferTime: typeof standardBufferTime === 'number' ? standardBufferTime : null, vehicleTariffs: vehicle.tariffs as Record<string, unknown>[] | undefined, vehiclePrice: vehicle.price, extraKmPrice: vehicle.extraKmPrice, vehicleBaseKmDay: vehicle.baseKmDay, seasonsTariffsMatrix, deliveryPrice: 0, roundResults, tariffPriceRoundingValue, tariffRoundPriceUp,});Remove unused fields from the settings destructuring if lint reports them as unused:
tariffDistanceRoundingValue,tariffRoundDistanceUp,hiddenInsurance,hiddenInsuranceValue,- Step 7: Return included
In the returned available quote object, add:
included: pricing.included,- Step 8: Remove unused calculator import
In pages/api/public/broker/availability.ts, remove calculateTotalPricePure from this import:
// @ts-expect-error no declaration fileimport { calculateTotalPricePure, getResultSeasonMatrix } from '@power-rent/lib/calculators';It should become:
// @ts-expect-error no declaration fileimport { getResultSeasonMatrix } from '@power-rent/lib/calculators';- Step 9: Run targeted availability tests
Run: yarn test _tests_/pages/api/public/broker/availability.test.ts
Expected: PASS.
Task 3: Align Broker Order Creation With Quote Helper
Files:
-
Modify:
pages/api/public/broker/create-order.ts -
Modify:
_tests_/pages/api/public/broker/create-order.test.ts -
Step 1: Update order test mocks
In _tests_/pages/api/public/broker/create-order.test.ts, extend the hoisted mocks:
const { mockFaunaQuery, mockFaunaClose, mockComposeOrderInput, mockPostOrderMutation, mockGetOrderDatesArrayAndDaysMigrated, mockGetDefaultVehicleParametersPure, mockSentryCapture, mockCalculateOfferGeneratorPrice,} = vi.hoisted(() => ({ mockFaunaQuery: vi.fn(), mockFaunaClose: vi.fn(), mockComposeOrderInput: vi.fn(), mockPostOrderMutation: vi.fn(), mockGetOrderDatesArrayAndDaysMigrated: vi.fn(), mockGetDefaultVehicleParametersPure: vi.fn(), mockSentryCapture: vi.fn(), mockCalculateOfferGeneratorPrice: vi.fn(),}));Add:
vi.mock('../../../../../pages/api/public/broker/offer-generator-pricing', () => ({ calculateBrokerOfferGeneratorPrice: mockCalculateOfferGeneratorPrice,}));In setupHappyPath(), add:
mockCalculateOfferGeneratorPrice.mockReturnValue({ totalPrice: 600, dailyPrice: 100, days: 6, extraKmPrice: 0.5, baseKmDay: 200, included: 1200, unlimitedKilometers: false,});- Step 2: Write failing order composition test
Append after the happy-path test:
it('uses offer generator quote values when composing broker order', async () => { setupHappyPath();
const { req, res, getStatus } = createReqRes(); await handler(req, res);
expect(getStatus()).toBe(201); expect(mockCalculateOfferGeneratorPrice).toHaveBeenCalledWith(expect.objectContaining({ dateFrom: '2026-04-01T00:00:00.000Z', dateTo: '2026-04-07T00:00:00.000Z', tarifficationVariant: 'standard', standardBufferTime: 60, vehicleTariffs: [], vehiclePrice: 100, extraKmPrice: 0.5, vehicleBaseKmDay: 200, deliveryPrice: 0, roundResults: true, tariffPriceRoundingValue: 1, tariffRoundPriceUp: true, })); expect(mockComposeOrderInput).toHaveBeenCalledWith(expect.objectContaining({ extraKmPrice: 0.5, days: 6, vehicle: expect.objectContaining({ price: 100, extraKmPrice: 0.5, baseKmDay: 200, tariffs: [], }), }));});- Step 3: Run failing order test
Run: yarn test _tests_/pages/api/public/broker/create-order.test.ts
Expected: FAIL because create-order.ts does not call the offer generator helper.
- Step 4: Import helper in create-order route
In pages/api/public/broker/create-order.ts, add:
import { calculateBrokerOfferGeneratorPrice } from './offer-generator-pricing';- Step 5: Build quote-equivalent pricing before partner creation
After the const { days } = await getOrderDatesArrayAndDaysMigrated(...) block, add:
const dateFromISO = startTime ? new Date(`${startDate}T${startTime}:00.000Z`).toISOString() : new Date(startDate).toISOString();const dateToISO = endTime ? new Date(`${endDate}T${endTime}:00.000Z`).toISOString() : new Date(endDate).toISOString();
const offerGeneratorPricing = calculateBrokerOfferGeneratorPrice({ dateFrom: dateFromISO, dateTo: dateToISO, tarifficationVariant, standardBufferTime, vehicleTariffs: vehicle.tariffs || [], vehiclePrice: vehicle.price, extraKmPrice: vehicle.extraKmPrice, vehicleBaseKmDay: vehicle.baseKmDay, seasonsTariffsMatrix: vehicle.seasonsTariffsMatrix || null, deliveryPrice: deliveryPrice || 0, roundResults, tariffPriceRoundingValue, tariffRoundPriceUp,});- Step 6: Keep
getDefaultVehicleParametersPureonly if needed
Replace:
const { extraKmPrice } = getDefaultVehicleParametersPure({ vehicleTariffs: vehicle.tariffs || [], vehiclePrice: vehicle.price, extraKmPrice: vehicle.extraKmPrice, vehicleBaseKmDay: vehicle.baseKmDay, days,});with:
const { extraKmPrice } = offerGeneratorPricing;If getDefaultVehicleParametersPure is now unused in create-order.ts, remove it from the imports.
- Step 7: Reuse precomputed dates in composeOrderInput
In the composeOrderInput call, replace repeated date expressions:
dateFrom: startTime ? new Date(`${startDate}T${startTime}:00.000Z`).toISOString() : new Date(startDate).toISOString(),dateTo: endTime ? new Date(`${endDate}T${endTime}:00.000Z`).toISOString() : new Date(endDate).toISOString(),with:
dateFrom: dateFromISO,dateTo: dateToISO,- Step 8: Run targeted order tests
Run: yarn test _tests_/pages/api/public/broker/create-order.test.ts
Expected: PASS.
Task 4: Broker Portal Contract Test
Files:
-
Modify:
apps/broker-portal/src/api/__tests__/vehicle-routes.test.ts -
Optional modify:
@power-rent/broker-typessource if typecheck requiresincluded. -
Step 1: Assert existing quote returns base km day
In apps/broker-portal/src/api/__tests__/vehicle-routes.test.ts, in returns tariff-based pricing with real commission, add:
expect(body.data.baseKmDay).toBe(200)- Step 2: Add pass-through test for offer-generator-compatible fields
Append in describe('GET /api/vehicles/:id/quote', () => { ... }):
it('passes through offer-generator-compatible quote fields', async () => { vi.mocked(prisma.vehicle.findUnique).mockResolvedValue(mockVehicleForQuote as any) mockGetBookingQuote.mockResolvedValue({ vehicleId: 'src-1', totalPrice: 450, currency: 'EUR', dailyPrice: 90, days: 5, extraKmPrice: 1.5, baseKmDay: 300, included: 1500, unlimitedKilometers: false, })
const res = await app.fetch( new Request('http://localhost/api/vehicles/v-uuid-1/quote?dateFrom=2026-04-01&dateTo=2026-04-06', { headers: { cookie: 'broker-session=mock-jwt' }, }) )
expect(res.status).toBe(200) const body = await res.json() expect(body.data).toMatchObject({ totalPrice: 450, dailyPrice: 90, days: 5, extraKmPrice: 1.5, baseKmDay: 300, unlimitedKilometers: false, })})- Step 3: Decide whether broker route should expose
included
If BookingQuoteResult includes included, update apps/broker-portal/src/lib/power-rent-client.http.ts return value:
included: vehicle.included,Then update apps/broker-portal/src/api/routes/vehicles.ts response:
included: quoteResult.included,If BookingQuoteResult does not include included and product does not need it in broker portal, leave broker portal response unchanged. The upstream /api/public/broker/availability still returns included for parity with offer generator.
- Step 4: Run broker portal route tests
Run: yarn test apps/broker-portal/src/api/__tests__/vehicle-routes.test.ts
Expected: PASS.
Task 5: Verification
Files:
-
No code changes.
-
Step 1: Run targeted tests
Run: yarn test _tests_/pages/api/public/broker/availability.test.ts _tests_/pages/api/public/broker/create-order.test.ts apps/broker-portal/src/api/__tests__/vehicle-routes.test.ts
Expected: PASS.
- Step 2: Run lint
Run: yarn lint
Expected: PASS. Fix only issues caused by this change.
- Step 3: Run TypeScript check
Run: yarn typecheck
Expected: PASS. Fix only issues caused by this change.
- Step 4: Run Flow check
Run: yarn flow:check
Expected: PASS. Fix only issues caused by this change.
- Step 5: Run full tests
Run: yarn test
Expected: PASS, except documented pre-existing Cloudinary mock-value failures if they appear in this environment.
- Step 6: Diff review
Run: git diff --stat && git diff -- pages/api/public/broker/offer-generator-pricing.ts pages/api/public/broker/availability.ts pages/api/public/broker/create-order.ts _tests_/pages/api/public/broker/availability.test.ts _tests_/pages/api/public/broker/create-order.test.ts apps/broker-portal/src/api/__tests__/vehicle-routes.test.ts
Expected: changes are scoped to broker offer-generator pricing alignment.
Acceptance Criteria
- Broker availability quote uses the same pricing sequence as offer generator initial result rows.
- Broker-created orders use the same quote-equivalent values before composing order input.
- Vehicle tariffs remain the only tariff source at runtime, matching offer generator behavior.
settings.daysTariffsMatrixis not used directly for broker runtime pricing.- Vehicle seasonal matrix behavior is preserved.
- Targeted tests, lint, typecheck, flow check, and unit tests pass or only show documented pre-existing failures.
Unresolved Questions
- Expose
includedfrom broker portal/api/vehicles/:id/quoteresponse, or only upstream availability?