@ticketboothapp/booking 1.2.133 → 1.2.135

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.133",
3
+ "version": "1.2.135",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -554,6 +554,7 @@ export function AdminChangeBookingFlow({
554
554
  providerPricingOverrides,
555
555
  adminCustomLinesAsAdditionalAdjustments,
556
556
  mergedProviderAdditionalAdjustments,
557
+ adminStructuredAdjustments,
557
558
  unchangedReceiptAnchor,
558
559
  showChangeFlowManualPriceLines,
559
560
  showAdminCustomLineEditor,
@@ -870,6 +871,7 @@ export function AdminChangeBookingFlow({
870
871
  displayChangeFlowProposedTotalWithEditableLines,
871
872
  providerPricingOverrides,
872
873
  mergedProviderAdditionalAdjustments,
874
+ adminStructuredAdjustments,
873
875
  providerApplyAuthoritativeReceipt,
874
876
  onSuccess,
875
877
  totalPrice,
@@ -9,10 +9,17 @@ export type AdminCustomReceiptLine = {
9
9
  label: string;
10
10
  amountInput: string;
11
11
  amountSign: 1 | -1;
12
+ calculation: 'FIXED_AMOUNT' | 'PERCENTAGE';
13
+ scope: 'POSITIVE_AMENDMENT_VALUE' | 'CURRENT_ACTIVE_TICKETS' | 'CURRENT_ACTIVE_BOOKING';
14
+ taxBehavior: 'NON_TAXABLE' | 'TAXABLE' | 'PRE_TAX_DISCOUNT' | 'POST_TAX_DISCOUNT';
15
+ reasonCode: string;
12
16
  };
13
17
 
14
18
  type AdminCustomReceiptLinePatch = Partial<
15
- Pick<AdminCustomReceiptLine, 'label' | 'amountInput' | 'amountSign'>
19
+ Pick<
20
+ AdminCustomReceiptLine,
21
+ 'label' | 'amountInput' | 'amountSign' | 'calculation' | 'scope' | 'taxBehavior' | 'reasonCode'
22
+ >
16
23
  >;
17
24
 
18
25
  export interface AdminChangePricingMessagesProps {
@@ -181,6 +188,19 @@ export function AdminChangePricingAdjustments({
181
188
  onUpdateAdminCustomReceiptLine(adj.id, { label: e.target.value })
182
189
  }
183
190
  />
191
+ <select
192
+ className="rounded-md border border-stone-300 bg-white px-2 py-1 text-xs text-stone-700"
193
+ aria-label="Adjustment calculation"
194
+ value={adj.calculation}
195
+ onChange={(e) =>
196
+ onUpdateAdminCustomReceiptLine(adj.id, {
197
+ calculation: e.target.value as AdminCustomReceiptLine['calculation'],
198
+ })
199
+ }
200
+ >
201
+ <option value="FIXED_AMOUNT">Fixed amount</option>
202
+ <option value="PERCENTAGE">Percentage</option>
203
+ </select>
184
204
  <div
185
205
  className="flex shrink-0 rounded-lg border border-stone-300 bg-stone-100/90 p-0.5 shadow-sm"
186
206
  role="group"
@@ -215,7 +235,7 @@ export function AdminChangePricingAdjustments({
215
235
  type="text"
216
236
  inputMode="decimal"
217
237
  className="admin-custom-receipt-input-amount w-[7rem] shrink-0 rounded-md border border-stone-300 bg-white text-right text-sm font-medium tabular-nums text-stone-800 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-400/80"
218
- placeholder="0.00"
238
+ placeholder={adj.calculation === 'PERCENTAGE' ? '10' : '0.00'}
219
239
  aria-label="Amount"
220
240
  value={adj.amountInput}
221
241
  onChange={(e) =>
@@ -224,6 +244,38 @@ export function AdminChangePricingAdjustments({
224
244
  })
225
245
  }
226
246
  />
247
+ <span className="text-xs text-stone-500" aria-hidden>
248
+ {adj.calculation === 'PERCENTAGE' ? '%' : ''}
249
+ </span>
250
+ <select
251
+ className="rounded-md border border-stone-300 bg-white px-2 py-1 text-xs text-stone-700"
252
+ aria-label="Adjustment scope"
253
+ value={adj.scope}
254
+ onChange={(e) =>
255
+ onUpdateAdminCustomReceiptLine(adj.id, {
256
+ scope: e.target.value as AdminCustomReceiptLine['scope'],
257
+ })
258
+ }
259
+ >
260
+ <option value="POSITIVE_AMENDMENT_VALUE">Added/change value</option>
261
+ <option value="CURRENT_ACTIVE_TICKETS">All active tickets</option>
262
+ <option value="CURRENT_ACTIVE_BOOKING">Entire active booking</option>
263
+ </select>
264
+ <select
265
+ className="rounded-md border border-stone-300 bg-white px-2 py-1 text-xs text-stone-700"
266
+ aria-label="Tax treatment"
267
+ value={adj.taxBehavior}
268
+ onChange={(e) =>
269
+ onUpdateAdminCustomReceiptLine(adj.id, {
270
+ taxBehavior: e.target.value as AdminCustomReceiptLine['taxBehavior'],
271
+ })
272
+ }
273
+ >
274
+ <option value="NON_TAXABLE">Non-taxable</option>
275
+ <option value="TAXABLE">Taxable</option>
276
+ <option value="PRE_TAX_DISCOUNT">Pre-tax discount</option>
277
+ <option value="POST_TAX_DISCOUNT">Post-tax discount</option>
278
+ </select>
227
279
  <button
228
280
  type="button"
229
281
  className="admin-custom-receipt-remove ml-auto shrink-0 rounded-md text-stone-400 hover:bg-stone-100 hover:text-stone-700 sm:ml-0"
@@ -76,6 +76,68 @@ function mapOriginalReceiptLines(originalReceipt: OriginalReceipt): PriceSummary
76
76
  return withStoredReceiptBreakdowns(mapQuoteLineItemsToPriceSummaryLines(originalReceipt.lineItems));
77
77
  }
78
78
 
79
+ function lineIdentity(line: PriceSummaryLine): string {
80
+ return line.kind === 'ticket'
81
+ ? `ticket:${line.category.trim().toUpperCase()}`
82
+ : `line:${String(line.type ?? '').trim().toUpperCase()}:${line.label.trim().toUpperCase()}`;
83
+ }
84
+
85
+ function lineAmount(line: PriceSummaryLine): number {
86
+ return line.kind === 'ticket' ? line.itemTotal : line.amount;
87
+ }
88
+
89
+ function lineQuantity(line: PriceSummaryLine): number | null {
90
+ return line.kind === 'ticket' ? line.qty : line.quantity ?? null;
91
+ }
92
+
93
+ function amendmentDeltaLines(
94
+ originalLines: PriceSummaryLine[],
95
+ proposedLines: PriceSummaryLine[],
96
+ ): PriceSummaryLine[] {
97
+ const remainingOriginal = new Map<string, { amount: number; quantity: number | null }>();
98
+ for (const line of originalLines) {
99
+ const key = lineIdentity(line);
100
+ const previous = remainingOriginal.get(key);
101
+ remainingOriginal.set(key, {
102
+ amount: roundMoney((previous?.amount ?? 0) + lineAmount(line)),
103
+ quantity:
104
+ previous?.quantity == null && lineQuantity(line) == null
105
+ ? null
106
+ : (previous?.quantity ?? 0) + (lineQuantity(line) ?? 0),
107
+ });
108
+ }
109
+
110
+ return proposedLines.flatMap((line): PriceSummaryLine[] => {
111
+ const original = remainingOriginal.get(lineIdentity(line));
112
+ const amount = roundMoney(lineAmount(line) - (original?.amount ?? 0));
113
+ const proposedQuantity = lineQuantity(line);
114
+ const quantity =
115
+ proposedQuantity == null && original?.quantity == null
116
+ ? null
117
+ : (proposedQuantity ?? 0) - (original?.quantity ?? 0);
118
+ if (Math.abs(amount) < 0.005 && (quantity == null || quantity === 0)) return [];
119
+
120
+ if (line.kind === 'ticket') {
121
+ return [{
122
+ ...line,
123
+ qty: Math.abs(quantity ?? 0) || line.qty,
124
+ itemTotal: amount,
125
+ editable: false,
126
+ lineKey: undefined,
127
+ }];
128
+ }
129
+ const isTax = String(line.type ?? '').toUpperCase() === 'TAX';
130
+ return [{
131
+ ...line,
132
+ label: isTax ? (amount >= 0 ? 'Additional tax' : 'Tax reduction') : line.label,
133
+ amount,
134
+ quantity: quantity == null ? null : Math.abs(quantity),
135
+ editable: false,
136
+ lineKey: undefined,
137
+ }];
138
+ });
139
+ }
140
+
79
141
  export function AdminChangeReceiptComparison({
80
142
  originalReceipt,
81
143
  newReceipt,
@@ -88,11 +150,6 @@ export function AdminChangeReceiptComparison({
88
150
  t,
89
151
  taxRate,
90
152
  newReceiptAdjustments,
91
- lineAmountInputs,
92
- onLineAmountInputChange,
93
- onLineAmountInputBlur,
94
- onLineAmountReset,
95
- lineLabelInputs,
96
153
  }: AdminChangeReceiptComparisonProps) {
97
154
  const displayLocale = normalizeLocale(locale);
98
155
  const originalReceiptLines = useMemo(
@@ -100,6 +157,21 @@ export function AdminChangeReceiptComparison({
100
157
  [originalReceipt],
101
158
  );
102
159
  const originalLinesIncludeTaxRow = hasTaxLine(originalReceiptLines);
160
+ const changeLines = useMemo(
161
+ () => amendmentDeltaLines(originalReceiptLines, newPriceSummaryLines),
162
+ [newPriceSummaryLines, originalReceiptLines],
163
+ );
164
+ const changeLinesIncludeTaxRow = hasTaxLine(changeLines);
165
+ const changeTax = roundMoney(
166
+ changeLines.reduce(
167
+ (sum, line) =>
168
+ line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
169
+ ? sum + line.amount
170
+ : sum,
171
+ 0,
172
+ ),
173
+ );
174
+ const changeSubtotal = roundMoney(amountDue - changeTax);
103
175
  const originalCurrency = originalReceipt.currency ?? currency;
104
176
  const amountDueClass =
105
177
  amountDue < -0.005
@@ -113,7 +185,7 @@ export function AdminChangeReceiptComparison({
113
185
  <div className="grid gap-4 md:grid-cols-2">
114
186
  <section className="min-w-0 overflow-visible rounded-lg border border-stone-200 bg-stone-50/70 p-3">
115
187
  <div className="mb-3 flex items-center justify-between gap-3">
116
- <h3 className="text-sm font-semibold text-stone-900">Current receipt</h3>
188
+ <h3 className="text-sm font-semibold text-stone-900">Existing purchase</h3>
117
189
  <span className="shrink-0 text-xs font-medium text-stone-500">
118
190
  {formatCurrencyAmount(originalReceipt.total, originalCurrency, displayLocale)}
119
191
  </span>
@@ -130,32 +202,60 @@ export function AdminChangeReceiptComparison({
130
202
  t={t}
131
203
  />
132
204
  </section>
133
- <section className="min-w-0 overflow-visible rounded-lg border border-stone-300 bg-white p-3 shadow-sm">
205
+ <section className="min-w-0 overflow-visible rounded-lg border border-sky-200 bg-sky-50/50 p-3">
134
206
  <div className="mb-3 flex items-center justify-between gap-3">
135
- <h3 className="text-sm font-semibold text-stone-900">New receipt</h3>
207
+ <div>
208
+ <h3 className="text-sm font-semibold text-stone-900">This change</h3>
209
+ <p className="mt-0.5 text-xs text-stone-500">Only newly charged, credited, or repriced value</p>
210
+ </div>
136
211
  <span className="shrink-0 text-xs font-medium text-stone-500">
137
- {formatCurrencyAmount(newReceipt.total, currency, displayLocale)}
212
+ {formatCurrencyAmount(amountDue, currency, displayLocale)}
138
213
  </span>
139
214
  </div>
140
215
  <PriceSummary
141
- lines={newPriceSummaryLines}
142
- total={newReceipt.total}
216
+ lines={changeLines}
217
+ total={amountDue}
143
218
  currency={currency}
144
219
  locale={displayLocale}
145
- subtotal={newReceipt.subtotal}
146
- taxAmount={!newPriceSummaryLinesIncludeTaxRow ? newReceipt.tax : 0}
220
+ subtotal={changeSubtotal}
221
+ taxAmount={!changeLinesIncludeTaxRow ? changeTax : 0}
147
222
  taxRate={taxRate}
148
223
  size="sm"
149
224
  t={t}
150
- extraBeforeSubtotal={newReceiptAdjustments}
151
- lineAmountInputs={lineAmountInputs}
152
- onLineAmountInputChange={onLineAmountInputChange}
153
- onLineAmountInputBlur={onLineAmountInputBlur}
154
- onLineAmountReset={onLineAmountReset}
155
- lineLabelInputs={lineLabelInputs}
156
225
  />
157
226
  </section>
158
227
  </div>
228
+ <section className="min-w-0 overflow-visible rounded-lg border border-stone-300 bg-white p-3 shadow-sm">
229
+ <div className="mb-3 flex items-center justify-between gap-3">
230
+ <div>
231
+ <h3 className="text-sm font-semibold text-stone-900">Updated booking receipt</h3>
232
+ <p className="mt-0.5 text-xs text-stone-500">Read-only pricing result; historical and added cohorts remain separate when supplied by the server</p>
233
+ </div>
234
+ <span className="shrink-0 text-xs font-medium text-stone-500">
235
+ {formatCurrencyAmount(newReceipt.total, currency, displayLocale)}
236
+ </span>
237
+ </div>
238
+ <PriceSummary
239
+ lines={newPriceSummaryLines.map((line) => ({ ...line, editable: false, lineKey: undefined }))}
240
+ total={newReceipt.total}
241
+ currency={currency}
242
+ locale={displayLocale}
243
+ subtotal={newReceipt.subtotal}
244
+ taxAmount={!newPriceSummaryLinesIncludeTaxRow ? newReceipt.tax : 0}
245
+ taxRate={taxRate}
246
+ size="sm"
247
+ t={t}
248
+ />
249
+ </section>
250
+ {newReceiptAdjustments ? (
251
+ <section className="rounded-lg border border-amber-200 bg-amber-50/40 p-3">
252
+ <div className="mb-2">
253
+ <h3 className="text-sm font-semibold text-stone-900">Admin adjustments</h3>
254
+ <p className="mt-0.5 text-xs text-stone-500">Optional structured charges or credits applied to this amendment</p>
255
+ </div>
256
+ {newReceiptAdjustments}
257
+ </section>
258
+ ) : null}
159
259
  <div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-stone-200 bg-stone-50 px-3 py-2">
160
260
  <span className="text-sm font-semibold text-stone-900">{amountDueLabel}</span>
161
261
  <span className={`text-lg font-semibold tabular-nums ${amountDueClass}`}>
@@ -32,6 +32,7 @@ export interface BuildAdminChangeProviderPayloadParams {
32
32
  newTotalAmount: number;
33
33
  providerPricingOverrides: AdminChangeProviderLineOverride[];
34
34
  mergedProviderAdditionalAdjustments: AdminChangeProviderAdditionalAdjustment[];
35
+ adminStructuredAdjustments: NonNullable<ProviderDashboardChangeBookingPayload['structuredAdjustments']>;
35
36
  providerApplyAuthoritativeReceipt?: ProviderDashboardChangeBookingPayload['authoritativeReceipt'];
36
37
  previousPassengerCount: number;
37
38
  previousAvailabilityId?: string | null;
@@ -52,6 +53,7 @@ export function buildAdminChangeProviderPayload({
52
53
  newTotalAmount,
53
54
  providerPricingOverrides,
54
55
  mergedProviderAdditionalAdjustments,
56
+ adminStructuredAdjustments,
55
57
  providerApplyAuthoritativeReceipt,
56
58
  previousPassengerCount,
57
59
  previousAvailabilityId,
@@ -90,6 +92,7 @@ export function buildAdminChangeProviderPayload({
90
92
  }
91
93
  : undefined,
92
94
  authoritativeReceipt: providerApplyAuthoritativeReceipt,
95
+ structuredAdjustments: adminStructuredAdjustments,
93
96
  capacitySeatCredit: {
94
97
  enabled: true,
95
98
  previousPassengerCount,
@@ -1,5 +1,35 @@
1
1
  import type { Currency } from './CurrencySwitcher';
2
2
 
3
+ export type AdminAmendmentAdjustmentMode =
4
+ | 'FIXED_AMOUNT'
5
+ | 'PERCENTAGE_DISCOUNT'
6
+ | 'PERCENTAGE_SURCHARGE';
7
+
8
+ export type AdminAmendmentAdjustmentScope =
9
+ | 'POSITIVE_AMENDMENT_VALUE'
10
+ | 'CURRENT_ACTIVE_TICKETS'
11
+ | 'CURRENT_ACTIVE_BOOKING'
12
+ | 'SELECTED_COMPONENTS';
13
+
14
+ export type AdminAmendmentTaxBehavior =
15
+ | 'NON_TAXABLE'
16
+ | 'TAXABLE'
17
+ | 'PRE_TAX_DISCOUNT'
18
+ | 'POST_TAX_DISCOUNT';
19
+
20
+ export type AdminAmendmentStructuredAdjustment = {
21
+ adjustmentId: string;
22
+ mode: AdminAmendmentAdjustmentMode;
23
+ fixedAmount?: number | null;
24
+ percentageBasisPoints?: number | null;
25
+ scope: AdminAmendmentAdjustmentScope;
26
+ selectedComponentIds?: string[];
27
+ taxBehavior: AdminAmendmentTaxBehavior;
28
+ reasonCode: string;
29
+ referenceCode?: string | null;
30
+ reason?: string | null;
31
+ };
32
+
3
33
  /** Payload passed to `onChangeBooking` when applying a dashboard-managed booking change. */
4
34
  export type ProviderDashboardChangeBookingPayload = {
5
35
  /** Parent catalog product id. Present when admin changes the booking to a different product. */
@@ -26,6 +56,15 @@ export type ProviderDashboardChangeBookingPayload = {
26
56
  /** Signed major-unit rows; may include admin custom receipt lines merged with provider inline adjustments. */
27
57
  additionalAdjustments?: Array<{ label: string; amount: number }>;
28
58
  } | null;
59
+ /** Server-resolved amendment adjustments. Percentage amounts are never calculated by the browser. */
60
+ structuredAdjustments?: AdminAmendmentStructuredAdjustment[];
61
+ /** Immutable adjustment ids selected for reversal. */
62
+ reverseAdjustmentIds?: string[];
63
+ /** Admin refund decision for removal operations returned by the quote preview. */
64
+ refundDispositionsByOperationId?: Record<
65
+ string,
66
+ 'REFUND_TO_ORIGINAL_PAYMENT' | 'PENDING_REFUND' | 'NO_REFUND'
67
+ >;
29
68
  /**
30
69
  * Admin/provider change flow: when FE-authoritative quoting is used, apply must persist
31
70
  * the exact receipt that was quoted instead of rebuilding from catalog pricing.
@@ -126,6 +126,7 @@ export interface UseAdminChangeCheckoutControllerParams {
126
126
  displayChangeFlowProposedTotalWithEditableLines: number;
127
127
  providerPricingOverrides: AdminChangeProviderLineOverride[];
128
128
  mergedProviderAdditionalAdjustments: AdminChangeProviderAdditionalAdjustment[];
129
+ adminStructuredAdjustments: NonNullable<ProviderDashboardChangeBookingPayload['structuredAdjustments']>;
129
130
  providerApplyAuthoritativeReceipt?: ProviderDashboardChangeBookingPayload['authoritativeReceipt'];
130
131
  onSuccess?: ChangeBookingFlowProps['onSuccess'];
131
132
  totalPrice: number;
@@ -223,6 +224,7 @@ export function useAdminChangeCheckoutController({
223
224
  displayChangeFlowProposedTotalWithEditableLines,
224
225
  providerPricingOverrides,
225
226
  mergedProviderAdditionalAdjustments,
227
+ adminStructuredAdjustments,
226
228
  providerApplyAuthoritativeReceipt,
227
229
  onSuccess,
228
230
  totalPrice,
@@ -320,6 +322,7 @@ export function useAdminChangeCheckoutController({
320
322
  newTotalAmount: displayChangeFlowProposedTotalWithEditableLines,
321
323
  providerPricingOverrides,
322
324
  mergedProviderAdditionalAdjustments,
325
+ adminStructuredAdjustments,
323
326
  providerApplyAuthoritativeReceipt,
324
327
  previousPassengerCount: changeFlowInitialTicketCount,
325
328
  previousAvailabilityId: initialValues?.availabilityId ?? null,
@@ -27,7 +27,12 @@ import { useAdminChangeProtectedPricing } from './useAdminChangeProtectedPricing
27
27
  import { useAdminChangePromoDiscount } from './useAdminChangePromoDiscount';
28
28
 
29
29
  type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
30
- type AdminCustomReceiptLine = { label: string; amountInput: string; amountSign?: number };
30
+ type AdminCustomReceiptLine = {
31
+ label: string;
32
+ amountInput: string;
33
+ amountSign?: number;
34
+ calculation: 'FIXED_AMOUNT' | 'PERCENTAGE';
35
+ };
31
36
  type ProviderPricingUi = NonNullable<
32
37
  NonNullable<ChangeBookingFlowProps['flowUi']>['providerDashboardChangePricingUi']
33
38
  >;
@@ -15,7 +15,12 @@ import type { Currency } from './CurrencySwitcher';
15
15
  import { normalizeLineLabelForCompare } from './change-booking-flow-helpers';
16
16
 
17
17
  type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
18
- type AdminCustomReceiptLine = { label: string; amountInput: string; amountSign?: number };
18
+ type AdminCustomReceiptLine = {
19
+ label: string;
20
+ amountInput: string;
21
+ amountSign?: number;
22
+ calculation: 'FIXED_AMOUNT' | 'PERCENTAGE';
23
+ };
19
24
 
20
25
  function addOnSelectionTotal(selections: AddOnSelection[] | null | undefined, addOns: AddOn[]): number {
21
26
  let sum = 0;
@@ -215,6 +220,7 @@ export function useAdminChangeProtectedPricing({
215
220
  () =>
216
221
  roundMoney(
217
222
  adminCustomReceiptLines.reduce((sum, line) => {
223
+ if (line.calculation !== 'FIXED_AMOUNT') return sum;
218
224
  const raw = line.amountInput.trim();
219
225
  if (raw === '' || raw === '+' || raw === '-') return sum;
220
226
  const n = Number(raw);
@@ -336,6 +336,7 @@ export function useAdminChangeQuoteDisplayState({
336
336
  providerPricingOverrides,
337
337
  adminCustomLinesAsAdditionalAdjustments,
338
338
  mergedProviderAdditionalAdjustments,
339
+ adminStructuredAdjustments,
339
340
  } = useAdminProviderPricingAdjustments({
340
341
  isProviderDashboardChange,
341
342
  showProviderPricingInlineEditor,
@@ -425,6 +426,7 @@ export function useAdminChangeQuoteDisplayState({
425
426
  providerPricingOverrides,
426
427
  adminCustomLinesAsAdditionalAdjustments,
427
428
  mergedProviderAdditionalAdjustments,
429
+ adminStructuredAdjustments,
428
430
  unchangedReceiptAnchor,
429
431
  showChangeFlowManualPriceLines,
430
432
  showAdminCustomLineEditor,
@@ -5,10 +5,17 @@ export type AdminCustomReceiptLine = {
5
5
  label: string;
6
6
  amountInput: string;
7
7
  amountSign: 1 | -1;
8
+ calculation: 'FIXED_AMOUNT' | 'PERCENTAGE';
9
+ scope: 'POSITIVE_AMENDMENT_VALUE' | 'CURRENT_ACTIVE_TICKETS' | 'CURRENT_ACTIVE_BOOKING';
10
+ taxBehavior: 'NON_TAXABLE' | 'TAXABLE' | 'PRE_TAX_DISCOUNT' | 'POST_TAX_DISCOUNT';
11
+ reasonCode: string;
8
12
  };
9
13
 
10
14
  type AdminCustomReceiptLinePatch = Partial<
11
- Pick<AdminCustomReceiptLine, 'label' | 'amountInput' | 'amountSign'>
15
+ Pick<
16
+ AdminCustomReceiptLine,
17
+ 'label' | 'amountInput' | 'amountSign' | 'calculation' | 'scope' | 'taxBehavior' | 'reasonCode'
18
+ >
12
19
  >;
13
20
 
14
21
  export function useAdminCustomReceiptLines(resetKey: string | null | undefined) {
@@ -25,6 +32,10 @@ export function useAdminCustomReceiptLines(resetKey: string | null | undefined)
25
32
  label: '',
26
33
  amountInput: '',
27
34
  amountSign: 1,
35
+ calculation: 'FIXED_AMOUNT',
36
+ scope: 'POSITIVE_AMENDMENT_VALUE',
37
+ taxBehavior: 'NON_TAXABLE',
38
+ reasonCode: 'STAFF_ADJUSTMENT',
28
39
  },
29
40
  ]);
30
41
  }, []);
@@ -6,7 +6,19 @@ import type {
6
6
  } from './booking-flow-ui';
7
7
  import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
8
8
 
9
- type AdminCustomReceiptLine = { label: string; amountInput: string; amountSign?: number };
9
+ type AdminCustomReceiptLine = {
10
+ id: string;
11
+ label: string;
12
+ amountInput: string;
13
+ amountSign?: number;
14
+ calculation: 'FIXED_AMOUNT' | 'PERCENTAGE';
15
+ scope: 'POSITIVE_AMENDMENT_VALUE' | 'CURRENT_ACTIVE_TICKETS' | 'CURRENT_ACTIVE_BOOKING';
16
+ taxBehavior: 'NON_TAXABLE' | 'TAXABLE' | 'PRE_TAX_DISCOUNT' | 'POST_TAX_DISCOUNT';
17
+ reasonCode: string;
18
+ };
19
+ type AdminStructuredAdjustment = NonNullable<
20
+ ProviderDashboardChangeBookingPayload['structuredAdjustments']
21
+ >[number];
10
22
  type ProviderLineOverride = NonNullable<
11
23
  NonNullable<ProviderDashboardChangeBookingPayload['pricingAdjustment']>['lineOverrides']
12
24
  >[number];
@@ -124,9 +136,33 @@ export function useAdminProviderPricingAdjustments({
124
136
  [adminCustomReceiptLines],
125
137
  );
126
138
 
139
+ const adminStructuredAdjustments = useMemo(
140
+ (): AdminStructuredAdjustment[] =>
141
+ adminCustomReceiptLines.flatMap((line) => {
142
+ const value = Number(line.amountInput.trim());
143
+ if (!Number.isFinite(value) || value <= 0) return [];
144
+ const discount = (line.amountSign ?? 1) < 0;
145
+ const percentage = line.calculation === 'PERCENTAGE';
146
+ return [{
147
+ adjustmentId: line.id,
148
+ mode: percentage
149
+ ? discount ? 'PERCENTAGE_DISCOUNT' : 'PERCENTAGE_SURCHARGE'
150
+ : 'FIXED_AMOUNT',
151
+ ...(percentage
152
+ ? { percentageBasisPoints: Math.round(value * 100) }
153
+ : { fixedAmount: roundMoney((discount ? -1 : 1) * value) }),
154
+ scope: line.scope,
155
+ taxBehavior: line.taxBehavior,
156
+ reasonCode: line.reasonCode.trim() || 'STAFF_ADJUSTMENT',
157
+ reason: line.label.trim() || null,
158
+ }];
159
+ }),
160
+ [adminCustomReceiptLines],
161
+ );
162
+
127
163
  const mergedProviderAdditionalAdjustments = useMemo(
128
- () => [...providerAdditionalAdjustments, ...adminCustomLinesAsAdditionalAdjustments],
129
- [providerAdditionalAdjustments, adminCustomLinesAsAdditionalAdjustments],
164
+ () => providerAdditionalAdjustments,
165
+ [providerAdditionalAdjustments],
130
166
  );
131
167
 
132
168
  const hasEffectiveChangeSelection =
@@ -142,5 +178,6 @@ export function useAdminProviderPricingAdjustments({
142
178
  providerPricingOverrides,
143
179
  adminCustomLinesAsAdditionalAdjustments,
144
180
  mergedProviderAdditionalAdjustments,
181
+ adminStructuredAdjustments,
145
182
  };
146
183
  }
package/src/index.ts CHANGED
@@ -26,6 +26,10 @@ export type {
26
26
  export { PrivateShuttleBookingFlow } from './components/booking/PrivateShuttleBookingFlow';
27
27
  export type { BookingFlowUiOptions } from './components/booking/booking-flow-ui';
28
28
  export type {
29
+ AdminAmendmentAdjustmentMode,
30
+ AdminAmendmentAdjustmentScope,
31
+ AdminAmendmentStructuredAdjustment,
32
+ AdminAmendmentTaxBehavior,
29
33
  ProviderDashboardChangeBookingPayload,
30
34
  ProviderDashboardInitialBooking,
31
35
  } from './components/booking/provider-dashboard-change-booking';