Skip to content

Commit ed128e7

Browse files
Merge pull request #24 from peek-travel/feat/ticket-price-range
feat: add ticket price ranges and PricingMoney displayPrice
2 parents cd22ca9 + ac4109a commit ed128e7

16 files changed

Lines changed: 232 additions & 25 deletions

docs/external/pricing-api.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,17 @@ const peek = new PeekAccessService({ /* installId, jwtSecret, issuer, appId, gat
5858
const activities = await peek.getProductService().getAllActivities();
5959
const activity = activities[0]!;
6060
// activity.productId → the activityId
61-
// activity.tickets → [{ id, name }, ...] (the resourceOption ids)
61+
// activity.tickets → [{ id, name, minPrice, maxPrice }, ...] (resourceOptions)
6262
// activity.currency → "USD" (use this for fixed-price overrides)
6363
```
6464

65+
Each ticket also carries its current price range as `minPrice` / `maxPrice`
66+
(a `PricingMoney`, or `null` when Peek reports no range) — handy for seeding a
67+
fixed-price override or showing "from $X". Every `PricingMoney` read back from
68+
the gateway includes a `displayPrice` string (e.g. `"$50.00"`) alongside the raw
69+
`amount`/`currency`; it is display-only, so you never set it when *building* an
70+
override.
71+
6572
> **Money and percentages are strings, not numbers.** `"80.00"`, `"-25"` — never
6673
> `80` or `-25`. The API rejects floats, and strings avoid precision loss.
6774

docs/internal/ARCHITECTURE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ models in `src/models/peek/`, CNG models in `src/models/cng/`.
139139
`Product` (empty string for add-ons, which have none) — the field pricing
140140
consumers need to set the currency on fixed-price overrides.
141141

142+
Each `Product.tickets[]` (a `ProductTicket`) additionally carries `minPrice`
143+
and `maxPrice` (`PricingMoney | null`), mapped from the resourceOption's
144+
`priceRange { min max }`. They are `null` for add-on options (no range) and
145+
when Peek reports none. The shared, internal `toPricingMoney` helper
146+
(`src/internal/peek/money.ts`) maps a raw gateway money node
147+
(`{ amount, currency, formatted }`) into `PricingMoney`, carrying `formatted`
148+
across as the optional `displayPrice` display string; both the products and
149+
pricing converters reuse it so that mapping lives in one place.
150+
142151
`pricing` is a **write-only, primitives-only** triad
143152
(`src/internal/peek/pricing/`, model `src/models/peek/pricing.ts`). It wraps the
144153
four Peek pricing mutations — `createPricingEngine`, `updatePricingEngine`,

llms.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ final authority and may still reject the request.
9090

9191
- `getProductService()` — `getAllProducts()` (flat list of activities + add-ons;
9292
add-ons have `type === ADD_ON_PRODUCT_TYPE`); `Product.currency` carries the
93-
activity's ISO currency (empty for add-ons)
93+
activity's ISO currency (empty for add-ons). Each `Product.tickets[]` entry
94+
carries `minPrice`/`maxPrice` (`PricingMoney | null`, `null` for add-ons and
95+
when Peek reports no range); every `PricingMoney` includes a `displayPrice`
96+
string (e.g. `"$50.00"`) on values read back from the gateway.
9497
- `getAccountUserService()` — `getAll()`, `getById(userId)`
9598
- `getResourcePoolService()` — `getAll(mode?)`
9699
- `getTimeslotService()` — `getForDay(productId, date, filter?)`, `getById(id)`,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@peektravel/app-utilities",
3-
"version": "0.5.1",
3+
"version": "0.5.2",
44
"description": "GraphQL JS mapping utilities extracted from the Peek Pro Autopilot connector.",
55
"license": "MIT",
66
"repository": {

src/internal/peek/money.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Shared, pure helper for mapping a raw gateway money node
3+
* (`{ amount, currency, formatted }`) into the clean {@link PricingMoney} model.
4+
*
5+
* Internal only — never re-exported from `src/index.ts`. Converters across
6+
* resources (products, pricing overrides) reuse this so the `formatted →
7+
* displayPrice` mapping lives in exactly one place.
8+
*/
9+
import type { PricingMoney } from "../../models/peek/pricing.js";
10+
11+
/** A raw money node as returned by the gateway. */
12+
export interface RawMoney {
13+
amount: string;
14+
currency: string;
15+
/** Human-formatted display string (e.g. `"$50.00"`). */
16+
formatted?: string | null;
17+
}
18+
19+
/**
20+
* Maps a raw money node into a {@link PricingMoney}, carrying the gateway's
21+
* `formatted` string across as `displayPrice`. Returns `null` for an
22+
* absent/`null` input so callers can pass an optional wire field straight
23+
* through.
24+
*/
25+
export function toPricingMoney(raw?: RawMoney | null): PricingMoney | null {
26+
if (!raw) return null;
27+
const money: PricingMoney = { amount: raw.amount, currency: raw.currency };
28+
if (raw.formatted != null) money.displayPrice = raw.formatted;
29+
return money;
30+
}

src/internal/peek/pricing/pricing-converter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
ResolvedPricingOverride,
1717
ResolvedResourceOption,
1818
} from "../../../models/peek/pricing.js";
19+
import { toPricingMoney } from "../money.js";
1920
import {
2021
SPOTS_TAKEN_FILTER_TYPENAME,
2122
type RawActivityContext,
@@ -63,7 +64,7 @@ function fromResourceOption(
6364

6465
function fromResolvedOverride(override: RawResolvedOverride): ResolvedOverride {
6566
return "price" in override
66-
? { mode: "fixed", price: override.price }
67+
? { mode: "fixed", price: toPricingMoney(override.price)! }
6768
: { mode: "percentage", percentageAdjustment: override.percentageAdjustment };
6869
}
6970

src/internal/peek/pricing/pricing-queries.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export const UPSERT_PRICING_OVERRIDES_MUTATION = `
103103
price {
104104
amount
105105
currency
106+
formatted
106107
}
107108
}
108109
... on PricingOverridesActivityContextResourceOptionPercentageAdjustmentOverride {
@@ -206,7 +207,7 @@ export interface UpsertOverridesVariables {
206207

207208
/** A single resolved resource-option override as returned by the API. */
208209
export type RawResolvedOverride =
209-
| { price: { amount: string; currency: string } }
210+
| { price: { amount: string; currency: string; formatted?: string } }
210211
| { percentageAdjustment: string };
211212

212213
/** A single resolved filter as returned by the API (tagged by `__typename`). */
@@ -278,7 +279,11 @@ export function buildUpsertInput(input: UpsertOverridesInput): RawUpsertInput {
278279
order: override.order,
279280
resourceOptions: override.resourceOptions.map((option) =>
280281
option.mode === "fixed"
281-
? { id: option.id, price: option.price }
282+
? {
283+
id: option.id,
284+
// Send only the wire fields — `displayPrice` is read-only.
285+
price: { amount: option.price.amount, currency: option.price.currency },
286+
}
282287
: { id: option.id, percentageAdjustment: option.percentageAdjustment },
283288
),
284289
filters: override.filters,

src/internal/peek/products/product-converter.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* model. No I/O — straightforward, testable transformations.
44
*/
55
import { ADD_ON_PRODUCT_TYPE, type Product } from "../../../models/peek/product.js";
6+
import { toPricingMoney } from "../money.js";
67
import type { ActivityNode, ItemOptionNode } from "./product-queries.js";
78

89
/** Default display color applied to add-on products. */
@@ -25,6 +26,8 @@ function fromActivity(activity: ActivityNode): Product {
2526
tickets: (activity.resourceOptions ?? []).map((option) => ({
2627
id: option.id,
2728
name: option.name,
29+
minPrice: toPricingMoney(option.priceRange?.min),
30+
maxPrice: toPricingMoney(option.priceRange?.max),
2831
})),
2932
};
3033
}
@@ -54,7 +57,12 @@ export function fromItemOptionNodes(nodes: ItemOptionNode[]): Product[] {
5457
};
5558
grouped.set(itemId, product);
5659
}
57-
product.tickets.push({ id: node.id, name: node.name });
60+
product.tickets.push({
61+
id: node.id,
62+
name: node.name,
63+
minPrice: null,
64+
maxPrice: null,
65+
});
5866
}
5967

6068
return Array.from(grouped.values());

src/internal/peek/products/product-queries.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,30 @@ export const PRODUCTS_QUERY = `
1818
resourceOptions {
1919
id
2020
name
21+
priceRange {
22+
min {
23+
currency
24+
amount
25+
formatted
26+
}
27+
max {
28+
currency
29+
amount
30+
formatted
31+
}
32+
}
2133
}
2234
}
2335
}
2436
`;
2537

38+
/** A raw monetary value on the wire (a `PricingMoney` before conversion). */
39+
export interface RawMoneyNode {
40+
currency: string;
41+
amount: string;
42+
formatted?: string;
43+
}
44+
2645
/** A single activity node as returned by {@link PRODUCTS_QUERY}. */
2746
export interface ActivityNode {
2847
name: string;
@@ -32,7 +51,11 @@ export interface ActivityNode {
3251
type: string;
3352
colorHex: string;
3453
currency?: string;
35-
resourceOptions: Array<{ id: string; name: string }>;
54+
resourceOptions: Array<{
55+
id: string;
56+
name: string;
57+
priceRange?: { min?: RawMoneyNode | null; max?: RawMoneyNode | null } | null;
58+
}>;
3659
}
3760

3861
/** The `data` payload of {@link PRODUCTS_QUERY}. */

0 commit comments

Comments
 (0)