Skip to content

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 tarifficationVariant and standardBufferTime from 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) and calculatePriceWithSeasons(...).
  • If there is no season matrix, use price * days.
  • Set calculated total price to roundToHundreds(totalBasePrice) + deliveryPrice.
  • Set extraKmPrice to tariff/base extra km price.
  • Set baseKmDay to Infinity when unlimited, otherwise daily base km.
  • Set included to Math.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-132
  • apps/broker-portal/src/lib/power-rent-client.http.ts:82-110
  • pages/api/public/broker/availability.ts:96-119

Current broker order path:

  • apps/broker-portal/src/lib/power-rent-client.http.ts:117-145
  • pages/api/public/broker/create-order.ts:221-227
  • pages/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 as Math.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 * days
totalPrice = roundToHundreds(totalBasePrice) + deliveryPrice
baseKmDay = defaults.unlimitedKilometers ? Infinity : defaults.baseKmDay
included = 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 included in available vehicle quote responses.
  • Modify: pages/api/public/broker/create-order.ts
    • Use the same helper for quote-equivalent values before composing the order.
    • Pass helper-derived extraKmPrice and a pricing-normalized vehicle object into composeOrderInput(...) 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-types source if VehicleAvailability / BookingQuoteResult types do not include included.

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 routes
import {
calculatePriceWithSeasons,
getDaysAmountByISO,
getDefaultVehicleParametersPure,
getNumber,
roundNumber,
} from '@power-rent/lib/calculators';
// @ts-expect-error Flow module consumed from TS API routes
import { 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 included to 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 file
import { calculateTotalPricePure, getResultSeasonMatrix } from '@power-rent/lib/calculators';

It should become:

// @ts-expect-error no declaration file
import { 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 getDefaultVehicleParametersPure only 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-types source if typecheck requires included.

  • 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.daysTariffsMatrix is 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 included from broker portal /api/vehicles/:id/quote response, or only upstream availability?