@kahitsan/ksui 0.47.0 → 0.47.2

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.47.0",
3
+ "version": "0.47.2",
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",
@@ -2,7 +2,8 @@
2
2
  // and address, offers inputs when they are missing, and warns when withholding
3
3
  // is on but no TIN is known.
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
- import { render, fireEvent, waitFor } from "@solidjs/testing-library";
5
+ import { render, fireEvent, waitFor, screen } from "@solidjs/testing-library";
6
+ import { createSignal } from "solid-js";
6
7
  import TransactionForm, { type TransactionFormProps } from "./TransactionForm";
7
8
  import { isValidTin } from "./payee-tin";
8
9
 
@@ -102,15 +103,19 @@ beforeEach(() => vi.unstubAllGlobals());
102
103
  afterEach(() => vi.unstubAllGlobals());
103
104
 
104
105
  describe("isValidTin", () => {
105
- it("accepts 9 or 12 digits with or without dashes", () => {
106
+ it("accepts 9 digits with a 3 or 5 digit branch code, dashes optional", () => {
106
107
  expect(isValidTin("123456789")).toBe(true);
107
108
  expect(isValidTin("123-456-789")).toBe(true);
108
109
  expect(isValidTin("123-456-789-000")).toBe(true);
109
110
  expect(isValidTin("123456789000")).toBe(true);
111
+ expect(isValidTin("123-456-789-00043")).toBe(true);
112
+ expect(isValidTin("12345678900043")).toBe(true);
110
113
  });
111
114
  it("rejects other lengths and non-digits", () => {
112
115
  expect(isValidTin("12345678")).toBe(false);
113
116
  expect(isValidTin("1234567890")).toBe(false);
117
+ expect(isValidTin("1234567890001")).toBe(false);
118
+ expect(isValidTin("123456789000430")).toBe(false);
114
119
  expect(isValidTin("12a-456-789")).toBe(false);
115
120
  });
116
121
  it("treats blank as valid because the field is optional", () => {
@@ -221,4 +226,51 @@ describe("TransactionForm payee tax ID", () => {
221
226
  await Promise.resolve();
222
227
  expect(queryByTestId("form-payee-tin-input")).toBeNull();
223
228
  });
229
+
230
+ it("shows the newly picked payee's tax ID when switching payees", async () => {
231
+ const payees = [
232
+ { id: 1, name: "Alpha Mart", kind: "vendor", tin: "123-456-789-00043", address: "1 Main St" },
233
+ { id: 2, name: "Beta Shop", kind: "vendor", tin: null, address: null },
234
+ ];
235
+ vi.stubGlobal(
236
+ "fetch",
237
+ vi.fn(async (url: string) => {
238
+ const one = /\/api\/payees\/(\d+)/.exec(String(url));
239
+ const body = one ? payees.find((p) => p.id === Number(one[1])) : { data: payees };
240
+ return new Response(JSON.stringify(body), { status: 200 });
241
+ }),
242
+ );
243
+ const [payee, setPayee] = createSignal("");
244
+ const [payeeId, setPayeeId] = createSignal<number | null>(null);
245
+ const { findByTestId, queryByTestId } = render(() => (
246
+ <TransactionForm
247
+ {...tinProps({
248
+ get payee() {
249
+ return payee();
250
+ },
251
+ setPayee,
252
+ get payeeId() {
253
+ return payeeId();
254
+ },
255
+ setPayeeId,
256
+ })}
257
+ />
258
+ ));
259
+ const pick = async (id: number) => {
260
+ fireEvent.click(await screen.findByTestId("form-payee-picker-trigger"));
261
+ fireEvent.input(await screen.findByTestId("form-payee-picker-input"), { target: { value: "a" } });
262
+ fireEvent.click(await screen.findByTestId(`form-payee-picker-result-${id}`));
263
+ };
264
+
265
+ await pick(1);
266
+ expect((await findByTestId("form-payee-tin-readonly")).textContent).toContain("123-456-789-00043");
267
+
268
+ await pick(2);
269
+ await findByTestId("form-payee-tin-input");
270
+ expect(queryByTestId("form-payee-tin-readonly")).toBeNull();
271
+
272
+ await pick(1);
273
+ expect((await findByTestId("form-payee-tin-readonly")).textContent).toContain("123-456-789-00043");
274
+ expect(queryByTestId("form-payee-tin-input")).toBeNull();
275
+ });
224
276
  });
@@ -7,7 +7,7 @@
7
7
  // advanced-fields toggle entirely, for a caller that locks `category` to one
8
8
  // value and never needs EWT/sharing (e.g. a "record my own expense" surface).
9
9
 
10
- import { createEffect, createSignal, Show, For } from "solid-js";
10
+ import { batch, createEffect, createSignal, Show, For } from "solid-js";
11
11
  import X from "lucide-solid/icons/x";
12
12
  import Upload from "lucide-solid/icons/upload";
13
13
  import FileIcon from "lucide-solid/icons/file";
@@ -788,13 +788,16 @@ export default function TransactionForm(props: TransactionFormProps) {
788
788
  icon={Store}
789
789
  noun="payee"
790
790
  placeholder={catConfig().payeePlaceholder!}
791
- onChange={(p) => {
792
- setSelectedPayee(p);
793
- props.setPayee(p ? p.name : "");
794
- props.setPayeeId(p ? p.id : null);
795
- props.setPayeeTin?.("");
796
- props.setPayeeAddress?.("");
797
- }}
791
+ onChange={(p) =>
792
+ // One batch so the sync effect never sees the new name with the old id.
793
+ batch(() => {
794
+ setSelectedPayee(p);
795
+ props.setPayee(p ? p.name : "");
796
+ props.setPayeeId(p ? p.id : null);
797
+ props.setPayeeTin?.("");
798
+ props.setPayeeAddress?.("");
799
+ })
800
+ }
798
801
  />
799
802
  </FormField>
800
803
  <FormField label={props.category === "sale" ? "Invoice ID" : "Receipt / Ref #"}>
@@ -833,14 +836,14 @@ export default function TransactionForm(props: TransactionFormProps) {
833
836
  value={props.payeeTin ?? ""}
834
837
  onInput={(e) => props.setPayeeTin!(e.currentTarget.value)}
835
838
  class="w-full bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_60%,transparent)] border border-[color-mix(in_srgb,var(--ks-border,rgba(39,39,42,0.5))_60%,transparent)] px-3 py-3 text-sm text-[var(--ks-fg,#ffffff)] ks-hud-clip-button focus:outline-none focus:border-[color-mix(in_srgb,var(--ks-focus-ring,#c9a961)_50%,transparent)]"
836
- placeholder="000-000-000-000"
839
+ placeholder="000-000-000-00000"
837
840
  />
838
841
  <Show when={!isValidTin(props.payeeTin ?? "")}>
839
842
  <p
840
843
  data-testid="form-payee-tin-error"
841
844
  class="mt-1 text-[10px] text-[var(--ks-danger,#ef4444)]"
842
845
  >
843
- A TIN has 9 digits, or 12 with the branch code.
846
+ A TIN has 9 digits, plus a 3 or 5 digit branch code.
844
847
  </p>
845
848
  </Show>
846
849
  </FormField>
@@ -1,14 +1,14 @@
1
1
  // Tax-ID helpers shared by the transaction form and its hosts, so both sides
2
2
  // apply the same rule before a TIN is saved.
3
3
 
4
- const TIN_PATTERN = /^\d{9}(\d{3})?$/;
4
+ const TIN_PATTERN = /^\d{9}(\d{3}|\d{5})?$/;
5
5
 
6
6
  /** Trims the input and drops dashes and spaces: "123-456-789" -> "123456789". */
7
7
  export function normalizeTin(value: string): string {
8
8
  return value.replace(/[\s-]/g, "");
9
9
  }
10
10
 
11
- /** A TIN is 9 digits, or 12 with the branch code. Blank is valid (optional). */
11
+ /** A TIN is 9 digits plus an optional 3 or 5 digit branch code. Blank is valid (optional). */
12
12
  export function isValidTin(value: string): boolean {
13
13
  const digits = normalizeTin(value);
14
14
  return digits === "" || TIN_PATTERN.test(digits);