@kahitsan/ksui 0.39.2 → 0.39.3

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": "@kahitsan/ksui",
3
- "version": "0.39.2",
3
+ "version": "0.39.3",
4
4
  "description": "ksui is a standalone set of SolidJS UI components for KahitSan/Hilinga and any SolidJS app. Published to the public npm registry and consumed as a normal dependency. Ships source under a `solid` export condition so the consumer's vite-plugin-solid compiles it with only solid-js externalized; it depends on nothing but solid-js + lucide-solid and injects its own CSS.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -37,6 +37,7 @@ function voucher(over: Partial<VoucherOption> & Pick<VoucherOption, "id" | "code
37
37
  value: 20,
38
38
  max_discount_amount: null,
39
39
  applicable_packages: null,
40
+ applicable_package_lineages: null,
40
41
  minimum_purchase: 0,
41
42
  valid_from: null,
42
43
  valid_until: null,
@@ -379,6 +380,52 @@ describe("VoucherPicker dialog", () => {
379
380
  );
380
381
  });
381
382
 
383
+ it("accepts a voucher when each package matches by aligned lineage", async () => {
384
+ mockPagedFetch([
385
+ voucher({
386
+ id: 35,
387
+ code: "LINEAGEOK",
388
+ applicable_packages: [99],
389
+ applicable_package_lineages: ["day-pass"],
390
+ }),
391
+ ]);
392
+ const { getByTestId } = render(() => (
393
+ <VoucherPicker
394
+ selected={null}
395
+ onChange={vi.fn()}
396
+ subtotal={1000}
397
+ packageIds={[1]}
398
+ packageLineages={["day-pass"]}
399
+ />
400
+ ));
401
+ fireEvent.click(getByTestId("voucher-picker-trigger"));
402
+ await waitFor(() => expect(getByTestId("voucher-picker-result-35")).toBeTruthy());
403
+ });
404
+ it("rejects a voucher when aligned package lineage does not match", async () => {
405
+ mockPagedFetch([
406
+ voucher({
407
+ id: 36,
408
+ code: "LINEAGEBAD",
409
+ applicable_packages: [99],
410
+ applicable_package_lineages: ["day-pass"],
411
+ }),
412
+ ]);
413
+ const { getByTestId, queryByTestId } = render(() => (
414
+ <VoucherPicker
415
+ selected={null}
416
+ onChange={vi.fn()}
417
+ subtotal={1000}
418
+ packageIds={[1]}
419
+ packageLineages={["single-use"]}
420
+ />
421
+ ));
422
+ fireEvent.click(getByTestId("voucher-picker-trigger"));
423
+ await waitFor(() => expect(getByTestId("voucher-picker-inapplicable-36")).toBeTruthy());
424
+ expect(queryByTestId("voucher-picker-result-36")).toBeNull();
425
+ expect(getByTestId("voucher-picker-inapplicable-36").textContent).toContain(
426
+ "Doesn't cover every item",
427
+ );
428
+ });
382
429
  it("surfaces a load failure instead of rendering an empty list", async () => {
383
430
  vi.stubGlobal(
384
431
  "fetch",
@@ -23,6 +23,7 @@ export interface VoucherOption {
23
23
  value: string | number | null;
24
24
  max_discount_amount: string | number | null;
25
25
  applicable_packages: number[] | null;
26
+ applicable_package_lineages?: string[] | null;
26
27
  minimum_purchase: string | number;
27
28
  valid_from: string | null;
28
29
  valid_until: string | null;
@@ -42,6 +43,7 @@ interface VoucherPickerProps {
42
43
  onChange: (next: VoucherOption | null) => void;
43
44
  subtotal: number;
44
45
  packageIds: number[];
46
+ packageLineages?: (string | null)[];
45
47
  disabled?: boolean;
46
48
  compact?: boolean;
47
49
  /** Same-shape endpoint override (defaults to the vouchers plugin's own API) —
@@ -104,6 +106,7 @@ function ineligibilityReason(
104
106
  voucher: VoucherOption,
105
107
  subtotal: number,
106
108
  packageIds: number[],
109
+ packageLineages: (string | null)[],
107
110
  todayIso: string,
108
111
  ): string | null {
109
112
  if (!voucher.is_active) return "Inactive";
@@ -116,10 +119,17 @@ function ineligibilityReason(
116
119
  if (usageExhausted(voucher)) return "Fully redeemed";
117
120
  if (asNumber(voucher.minimum_purchase) > subtotal)
118
121
  return `Needs ${formatCurrency(asNumber(voucher.minimum_purchase))} minimum`;
119
- if (voucher.applicable_packages && voucher.applicable_packages.length > 0) {
122
+ const allowedIds = voucher.applicable_packages ?? [];
123
+ const allowedLineages = voucher.applicable_package_lineages ?? [];
124
+ if (allowedIds.length > 0 || allowedLineages.length > 0) {
120
125
  if (packageIds.length === 0) return "Only for specific items";
121
- const allowed = new Set(voucher.applicable_packages);
122
- if (!packageIds.every((id) => allowed.has(id))) return "Doesn't cover every item";
126
+ const allowed = new Set(allowedIds);
127
+ if (packageIds.some((id, index) =>
128
+ !allowed.has(id) &&
129
+ !(packageLineages[index] != null && allowedLineages.includes(packageLineages[index]!))
130
+ )) {
131
+ return "Doesn't cover every item";
132
+ }
123
133
  }
124
134
  return null;
125
135
  }
@@ -129,9 +139,10 @@ function isApplicable(
129
139
  voucher: VoucherOption,
130
140
  subtotal: number,
131
141
  packageIds: number[],
142
+ packageLineages: (string | null)[],
132
143
  todayIso: string,
133
144
  ): boolean {
134
- return ineligibilityReason(voucher, subtotal, packageIds, todayIso) === null;
145
+ return ineligibilityReason(voucher, subtotal, packageIds, packageLineages, todayIso) === null;
135
146
  }
136
147
 
137
148
  function formatVoucherDescription(v: VoucherOption): string {
@@ -293,16 +304,16 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
293
304
 
294
305
  const applicable = createMemo(() => {
295
306
  const today_ = today();
296
- return vouchers().filter((v) => isApplicable(v, props.subtotal, props.packageIds, today_));
307
+ return vouchers().filter((v) => isApplicable(v, props.subtotal, props.packageIds, props.packageLineages ?? [], today_));
297
308
  });
298
309
 
299
310
  const inapplicable = createMemo(() => {
300
311
  const today_ = today();
301
312
  return vouchers()
302
- .filter((v) => !isApplicable(v, props.subtotal, props.packageIds, today_))
313
+ .filter((v) => !isApplicable(v, props.subtotal, props.packageIds, props.packageLineages ?? [], today_))
303
314
  .map((v) => ({
304
315
  voucher: v,
305
- reason: ineligibilityReason(v, props.subtotal, props.packageIds, today_) ?? "",
316
+ reason: ineligibilityReason(v, props.subtotal, props.packageIds, props.packageLineages ?? [], today_) ?? "",
306
317
  }));
307
318
  });
308
319