@tuturuuu/ui 0.14.0 → 0.16.0

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.
Files changed (86) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/biome.json +1 -1
  3. package/package.json +62 -62
  4. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +12 -0
  5. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +93 -6
  6. package/src/components/ui/custom/loading-statistic-card.tsx +18 -0
  7. package/src/components/ui/custom/notification-popover-client.tsx +5 -1
  8. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.test.tsx +34 -0
  9. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +106 -0
  10. package/src/components/ui/custom/settings/sidebar-settings.tsx +18 -6
  11. package/src/components/ui/custom/settings-dialog-shell.tsx +18 -12
  12. package/src/components/ui/custom/sidebar-context.tsx +58 -3
  13. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +5 -3
  14. package/src/components/ui/custom/statistic-card.tsx +104 -0
  15. package/src/components/ui/custom/structure.test.tsx +29 -0
  16. package/src/components/ui/custom/structure.tsx +103 -88
  17. package/src/components/ui/finance/invoices/civil-month.ts +224 -0
  18. package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.test.tsx +49 -0
  19. package/src/components/ui/finance/invoices/components/subscription-invoice-proposal.tsx +81 -0
  20. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
  21. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +90 -2
  22. package/src/components/ui/finance/invoices/new-invoice-page.tsx +40 -4
  23. package/src/components/ui/finance/invoices/pending-columns.test.ts +26 -0
  24. package/src/components/ui/finance/invoices/pending-columns.tsx +45 -14
  25. package/src/components/ui/finance/invoices/standard-invoice.test.ts +31 -0
  26. package/src/components/ui/finance/invoices/standard-invoice.tsx +33 -16
  27. package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +331 -0
  28. package/src/components/ui/finance/invoices/subscription-invoice.tsx +53 -28
  29. package/src/components/ui/finance/invoices/utils.test.ts +84 -0
  30. package/src/components/ui/finance/invoices/utils.ts +56 -134
  31. package/src/components/ui/resizable.test.ts +15 -0
  32. package/src/components/ui/resizable.tsx +28 -6
  33. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +62 -0
  34. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +48 -0
  35. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +32 -2
  36. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +41 -3
  37. package/src/components/ui/tu-do/boards/boardId/menus/task-due-date-menu.tsx +31 -10
  38. package/src/components/ui/tu-do/boards/boardId/menus/task-priority-menu.tsx +7 -5
  39. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +7 -5
  40. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.test.ts +44 -0
  41. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-archive-visibility.ts +17 -0
  42. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +2 -0
  43. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +7 -0
  44. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +44 -30
  45. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +54 -0
  46. package/src/components/ui/tu-do/my-tasks/task-list-with-completion.tsx +43 -1
  47. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +33 -15
  48. package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +3 -3
  49. package/src/components/ui/tu-do/progress/task-progress-page.tsx +74 -28
  50. package/src/components/ui/tu-do/progress/task-progress-panels.tsx +12 -10
  51. package/src/components/ui/tu-do/projects/components/project-grid-card.tsx +22 -6
  52. package/src/components/ui/tu-do/projects/components/project-list-item.tsx +24 -8
  53. package/src/components/ui/tu-do/projects/projectId/components/__tests__/tasks-tab-layout.test.tsx +6 -0
  54. package/src/components/ui/tu-do/projects/projectId/components/tasks-tab.tsx +3 -1
  55. package/src/components/ui/tu-do/projects/projectId/task-project-detail-page-client.tsx +11 -3
  56. package/src/components/ui/tu-do/projects/projectId/task-project-detail.tsx +2 -0
  57. package/src/components/ui/tu-do/projects/projectId/types.ts +1 -0
  58. package/src/components/ui/tu-do/projects/task-projects-client.tsx +45 -1
  59. package/src/components/ui/tu-do/projects/types.ts +4 -1
  60. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +10 -0
  61. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -1
  62. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +87 -2
  63. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +27 -3
  64. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +41 -3
  65. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +54 -0
  66. package/src/components/ui/tu-do/shared/board-header.tsx +20 -0
  67. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
  68. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +2 -0
  69. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +7 -0
  70. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-mutations.test.tsx +231 -0
  71. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +139 -68
  72. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.test.tsx +68 -0
  73. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +17 -0
  74. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +45 -23
  75. package/src/components/ui/tu-do/shared/task-row-actions-menu.test.tsx +138 -0
  76. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +15 -9
  77. package/src/components/ui/tu-do/templates/client.tsx +97 -78
  78. package/src/components/ui/tu-do/templates/marketplace/client.tsx +171 -135
  79. package/src/components/ui/tu-do/templates/task-templates-hub.tsx +3 -0
  80. package/src/components/ui/tu-do/templates/templateId/client.tsx +24 -9
  81. package/src/components/ui/tu-do/templates/templateId/task-template-detail-page-client.tsx +8 -2
  82. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -0
  83. package/src/hooks/__tests__/use-task-actions.test.tsx +14 -39
  84. package/src/hooks/task-actions-personal-external.ts +12 -6
  85. package/src/hooks/use-notifications.ts +14 -2
  86. package/vitest.config.ts +4 -0
@@ -21,6 +21,40 @@ type PendingInvoiceRow = PendingInvoice & {
21
21
  group_names?: string[];
22
22
  };
23
23
 
24
+ export function buildPendingInvoiceCreateUrl({
25
+ attendanceDays,
26
+ financePrefix,
27
+ groupIds,
28
+ month,
29
+ potentialTotal,
30
+ useAttendanceBased,
31
+ userId,
32
+ wsId,
33
+ }: {
34
+ attendanceDays: number;
35
+ financePrefix: string;
36
+ groupIds: string[];
37
+ month: string;
38
+ potentialTotal: number;
39
+ useAttendanceBased: boolean;
40
+ userId: string;
41
+ wsId: string;
42
+ }) {
43
+ const searchParams = new URLSearchParams({
44
+ type: 'subscription',
45
+ user_id: userId,
46
+ });
47
+
48
+ if (month) searchParams.set('month', month);
49
+ if (useAttendanceBased) {
50
+ searchParams.set('billable_quantity', String(attendanceDays));
51
+ }
52
+ searchParams.set('suggested_total', String(potentialTotal));
53
+ if (groupIds.length > 0) searchParams.set('group_ids', groupIds.join(','));
54
+
55
+ return `/${wsId}${financePrefix}/invoices/new?${searchParams.toString()}`;
56
+ }
57
+
24
58
  function PendingInvoicePotentialTotalCell({
25
59
  amount,
26
60
  currency,
@@ -268,6 +302,7 @@ export const pendingInvoiceColumns = (
268
302
  ).filter((id: string) => Boolean(id));
269
303
  const monthsOwed = row.getValue<string[]>('months_owed');
270
304
  const attendanceDays = row.getValue<number>('attendance_days');
305
+ const potentialTotal = row.getValue<number>('potential_total') || 0;
271
306
 
272
307
  // Get the LAST (most recent) unpaid month from the array
273
308
  const lastUnpaidMonth =
@@ -277,20 +312,16 @@ export const pendingInvoiceColumns = (
277
312
 
278
313
  if (!wsId) return null;
279
314
 
280
- const searchParams = new URLSearchParams();
281
- searchParams.set('type', 'subscription');
282
- searchParams.set('user_id', userId);
283
- if (lastUnpaidMonth) {
284
- searchParams.set('month', lastUnpaidMonth);
285
- }
286
- if (useAttendanceBased) {
287
- searchParams.set('amount', String(attendanceDays));
288
- }
289
- if (groupIds.length > 0) {
290
- searchParams.set('group_ids', groupIds.join(','));
291
- }
292
-
293
- const createInvoiceUrl = `/${wsId}${financePrefix}/invoices/new?${searchParams.toString()}`;
315
+ const createInvoiceUrl = buildPendingInvoiceCreateUrl({
316
+ attendanceDays,
317
+ financePrefix,
318
+ groupIds,
319
+ month: lastUnpaidMonth ?? '',
320
+ potentialTotal,
321
+ useAttendanceBased,
322
+ userId,
323
+ wsId,
324
+ });
294
325
 
295
326
  return (
296
327
  <div className="flex items-center gap-2">
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveStandardInvoiceCategoryId } from './standard-invoice';
3
+
4
+ describe('resolveStandardInvoiceCategoryId', () => {
5
+ it('prefers a single linked product finance category', () => {
6
+ expect(
7
+ resolveStandardInvoiceCategoryId({
8
+ defaultCategoryId: 'category-default',
9
+ linkedFinanceCategoryIds: ['category-linked'],
10
+ })
11
+ ).toBe('category-linked');
12
+ });
13
+
14
+ it('requires explicit selection when linked product finance categories are mixed', () => {
15
+ expect(
16
+ resolveStandardInvoiceCategoryId({
17
+ defaultCategoryId: 'category-default',
18
+ linkedFinanceCategoryIds: ['category-a', 'category-b'],
19
+ })
20
+ ).toBe('');
21
+ });
22
+
23
+ it('falls back to the transaction default when products do not provide a category', () => {
24
+ expect(
25
+ resolveStandardInvoiceCategoryId({
26
+ defaultCategoryId: 'category-default',
27
+ linkedFinanceCategoryIds: [],
28
+ })
29
+ ).toBe('category-default');
30
+ });
31
+ });
@@ -57,6 +57,7 @@ interface Props {
57
57
  printAfterCreate?: boolean;
58
58
  downloadImageAfterCreate?: boolean;
59
59
  defaultWalletId?: string;
60
+ defaultCategoryId?: string;
60
61
  defaultCurrency?: string;
61
62
  canChangeFinanceWallets?: boolean;
62
63
  canSetFinanceWalletsOnCreate?: boolean;
@@ -71,6 +72,7 @@ export function StandardInvoice({
71
72
  printAfterCreate = false,
72
73
  downloadImageAfterCreate = false,
73
74
  defaultWalletId,
75
+ defaultCategoryId,
74
76
  defaultCurrency: rawDefaultCurrency = 'USD',
75
77
  canChangeFinanceWallets = true,
76
78
  canSetFinanceWalletsOnCreate = true,
@@ -194,7 +196,9 @@ export function StandardInvoice({
194
196
  }, [defaultWalletId]);
195
197
  const [selectedPromotionId, setSelectedPromotionId] =
196
198
  useState<string>('none');
197
- const [selectedCategoryId, setSelectedCategoryId] = useState<string>('');
199
+ const [selectedCategoryId, setSelectedCategoryId] = useState<string>(
200
+ defaultCategoryId || ''
201
+ );
198
202
  const [invoiceContent, setInvoiceContent] = useState<string>('');
199
203
  const [invoiceNotes, setInvoiceNotes] = useState<string>('');
200
204
  const [isCreating, setIsCreating] = useState(false);
@@ -280,7 +284,6 @@ export function StandardInvoice({
280
284
  [selectedProducts]
281
285
  );
282
286
  const hasMixedLinkedFinanceCategories = linkedFinanceCategoryIds.length > 1;
283
- const hasSingleLinkedFinanceCategory = linkedFinanceCategoryIds.length === 1;
284
287
 
285
288
  // Auto-generate invoice content based on selected products
286
289
  useEffect(() => {
@@ -318,19 +321,13 @@ export function StandardInvoice({
318
321
  }, [selectedUserId]);
319
322
 
320
323
  useEffect(() => {
321
- if (hasSingleLinkedFinanceCategory) {
322
- setSelectedCategoryId(linkedFinanceCategoryIds[0] ?? '');
323
- return;
324
- }
325
-
326
- if (hasMixedLinkedFinanceCategories) {
327
- setSelectedCategoryId('');
328
- }
329
- }, [
330
- hasMixedLinkedFinanceCategories,
331
- hasSingleLinkedFinanceCategory,
332
- linkedFinanceCategoryIds,
333
- ]);
324
+ setSelectedCategoryId(
325
+ resolveStandardInvoiceCategoryId({
326
+ defaultCategoryId,
327
+ linkedFinanceCategoryIds,
328
+ })
329
+ );
330
+ }, [defaultCategoryId, linkedFinanceCategoryIds]);
334
331
 
335
332
  useBestPromotionSelection({
336
333
  enabled: promotionsAllowed,
@@ -422,7 +419,7 @@ export function StandardInvoice({
422
419
  resetRounding();
423
420
  setSelectedUserId(null);
424
421
  setSelectedWalletId(defaultWalletId || '');
425
- setSelectedCategoryId('');
422
+ setSelectedCategoryId(defaultCategoryId || '');
426
423
  setCustomerSearch('');
427
424
  }
428
425
  } catch (error) {
@@ -657,3 +654,23 @@ export function StandardInvoice({
657
654
  </div>
658
655
  );
659
656
  }
657
+
658
+ export function resolveStandardInvoiceCategoryId({
659
+ defaultCategoryId,
660
+ linkedFinanceCategoryIds,
661
+ }: {
662
+ defaultCategoryId?: string;
663
+ linkedFinanceCategoryIds: string[];
664
+ }) {
665
+ const uniqueLinkedCategoryIds = [...new Set(linkedFinanceCategoryIds)];
666
+
667
+ if (uniqueLinkedCategoryIds.length === 1) {
668
+ return uniqueLinkedCategoryIds[0] ?? '';
669
+ }
670
+
671
+ if (uniqueLinkedCategoryIds.length > 1) {
672
+ return '';
673
+ }
674
+
675
+ return defaultCategoryId || '';
676
+ }
@@ -0,0 +1,331 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { render, waitFor } from '@testing-library/react';
3
+ import type { ComponentProps, PropsWithChildren } from 'react';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { SubscriptionInvoice } from './subscription-invoice';
6
+
7
+ const testState = vi.hoisted(() => {
8
+ const inventory = {
9
+ amount: 5,
10
+ min_amount: 1,
11
+ price: 100,
12
+ unit_id: 'unit-1',
13
+ unit_name: 'Seat',
14
+ warehouse_id: 'warehouse-1',
15
+ warehouse_name: 'Main',
16
+ };
17
+ const product = {
18
+ category: null,
19
+ category_id: 'inventory-category',
20
+ created_at: null,
21
+ description: null,
22
+ finance_category_id: null as string | null,
23
+ id: 'product-1',
24
+ inventory: [inventory],
25
+ manufacturer: null,
26
+ name: 'Subscription seat',
27
+ usage: null,
28
+ ws_id: 'ws-1',
29
+ };
30
+
31
+ return {
32
+ InvoicePaymentSettings: vi.fn(),
33
+ categories: [] as Array<{ id: string; name: string }>,
34
+ groupIds: ['group-1'],
35
+ month: '2026-07',
36
+ productSelectionInjected: false,
37
+ products: [product],
38
+ selectedProducts: [{ inventory, product, quantity: 1 }],
39
+ useCategories: vi.fn(),
40
+ useWallets: vi.fn(),
41
+ userId: 'user-1',
42
+ wallets: [] as Array<{
43
+ currency: string;
44
+ id: string;
45
+ name: string;
46
+ type: string;
47
+ }>,
48
+ };
49
+ });
50
+
51
+ vi.mock('next-intl', () => ({
52
+ useLocale: () => 'en-US',
53
+ useTranslations: () => (key: string) => key,
54
+ }));
55
+
56
+ vi.mock('next/navigation', () => ({
57
+ useRouter: () => ({ push: vi.fn() }),
58
+ }));
59
+
60
+ vi.mock('nuqs', () => ({
61
+ parseAsArrayOf: () => ({
62
+ withDefault: () => ({
63
+ withOptions: () => ({}),
64
+ }),
65
+ }),
66
+ parseAsString: {},
67
+ useQueryState: (key: string, options?: { defaultValue?: unknown }) => {
68
+ if (key === 'user_id') return [testState.userId, vi.fn()];
69
+ if (key === 'group_ids') return [testState.groupIds, vi.fn()];
70
+ if (key === 'month') return [testState.month, vi.fn()];
71
+ return [options?.defaultValue ?? '', vi.fn()];
72
+ },
73
+ }));
74
+
75
+ vi.mock('../../../../hooks/use-debounce', () => ({
76
+ useDebounce: (value: string) => [value],
77
+ }));
78
+
79
+ vi.mock('../finance-route-context', () => ({
80
+ useFinanceHref: () => (path: string) => path,
81
+ }));
82
+
83
+ vi.mock('../shared/use-finance-confidential-visibility', () => ({
84
+ useFinanceConfidentialVisibility: () => ({ isConfidential: false }),
85
+ }));
86
+
87
+ vi.mock('./components/invoice-blocked-state', () => ({
88
+ InvoiceBlockedState: () => null,
89
+ }));
90
+
91
+ vi.mock('./components/invoice-checkout-summary', () => ({
92
+ InvoiceCheckoutSummary: () => null,
93
+ }));
94
+
95
+ vi.mock('./components/invoice-content-editor', () => ({
96
+ InvoiceContentEditor: () => null,
97
+ }));
98
+
99
+ vi.mock('./components/invoice-customer-select-card', () => ({
100
+ InvoiceCustomerSelectCard: () => null,
101
+ }));
102
+
103
+ vi.mock('./components/invoice-payment-settings', () => ({
104
+ InvoicePaymentSettings: (props: unknown) => {
105
+ testState.InvoicePaymentSettings(props);
106
+ return null;
107
+ },
108
+ }));
109
+
110
+ vi.mock('./components/invoice-products-permission-warning', () => ({
111
+ InvoiceProductsPermissionWarning: () => null,
112
+ isPermissionRequestError: () => false,
113
+ }));
114
+
115
+ vi.mock('./components/subscription-attendance-summary', () => ({
116
+ SubscriptionAttendanceSummary: () => null,
117
+ }));
118
+
119
+ vi.mock('./components/subscription-group-selector', () => ({
120
+ SubscriptionGroupSelector: () => null,
121
+ }));
122
+
123
+ vi.mock('./components/subscription-prepaid-controls', () => ({
124
+ SubscriptionPrepaidControls: () => null,
125
+ }));
126
+
127
+ vi.mock('./create-promotion-dialog', () => ({
128
+ CreatePromotionDialog: () => null,
129
+ }));
130
+
131
+ vi.mock('./product-selection', () => ({
132
+ ProductSelection: (props: {
133
+ onSelectedProductsChange: (
134
+ products: typeof testState.selectedProducts
135
+ ) => void;
136
+ }) => {
137
+ if (!testState.productSelectionInjected) {
138
+ testState.productSelectionInjected = true;
139
+ queueMicrotask(() =>
140
+ props.onSelectedProductsChange(testState.selectedProducts)
141
+ );
142
+ }
143
+ return null;
144
+ },
145
+ }));
146
+
147
+ vi.mock('./hooks', () => ({
148
+ useAvailablePromotions: () => ({ data: [] }),
149
+ useCategories: (wsId: string, options?: unknown) => {
150
+ testState.useCategories(wsId, options);
151
+ return { data: testState.categories };
152
+ },
153
+ useInvoiceAttendanceConfig: () => ({ data: true }),
154
+ useInvoiceBlockedGroups: () => ({ data: [] }),
155
+ useInvoiceCustomerSearch: () => ({
156
+ customers: [],
157
+ error: null,
158
+ fetchNextPage: vi.fn(),
159
+ hasNextPage: false,
160
+ isFetching: false,
161
+ isFetchingNextPage: false,
162
+ isLoading: false,
163
+ selectedUser: { id: testState.userId, display_name: 'Customer' },
164
+ }),
165
+ useMultiGroupProducts: () => ({
166
+ data: [],
167
+ error: null,
168
+ isLoading: false,
169
+ }),
170
+ useProducts: () => ({
171
+ data: testState.products,
172
+ error: null,
173
+ isLoading: false,
174
+ }),
175
+ useSubscriptionInvoiceContext: () => ({
176
+ data: { attendance: [], latestInvoices: [] },
177
+ error: null,
178
+ isLoading: false,
179
+ }),
180
+ useUserGroups: () => ({
181
+ data: [
182
+ {
183
+ workspace_user_groups: {
184
+ ending_date: null,
185
+ id: 'group-1',
186
+ name: 'Group 1',
187
+ sessions: ['2026-07-05'],
188
+ starting_date: '2026-07-01',
189
+ },
190
+ },
191
+ ],
192
+ isLoading: false,
193
+ }),
194
+ useUserLinkedPromotions: () => ({ data: [] }),
195
+ useUserReferralDiscounts: () => ({ data: [] }),
196
+ useWallets: (wsId: string, options?: unknown) => {
197
+ testState.useWallets(wsId, options);
198
+ return { data: testState.wallets };
199
+ },
200
+ }));
201
+
202
+ vi.mock('./hooks/use-best-promotion-selection', () => ({
203
+ useBestPromotionSelection: () => undefined,
204
+ }));
205
+
206
+ vi.mock('./hooks/use-invoice-rounding', () => ({
207
+ useInvoiceRounding: () => ({
208
+ resetRounding: vi.fn(),
209
+ roundDown: vi.fn(),
210
+ roundUp: vi.fn(),
211
+ roundedTotal: 100,
212
+ }),
213
+ }));
214
+
215
+ vi.mock('./hooks/use-invoice-subtotal', () => ({
216
+ useInvoiceSubtotal: () => 100,
217
+ }));
218
+
219
+ vi.mock('./hooks/use-subscription-auto-selection', () => ({
220
+ useSubscriptionAutoSelection: () => undefined,
221
+ }));
222
+
223
+ vi.mock('./hooks/use-subscription-invoice-content', () => ({
224
+ useSubscriptionInvoiceContent: () => undefined,
225
+ }));
226
+
227
+ function createQueryClient() {
228
+ return new QueryClient({
229
+ defaultOptions: {
230
+ queries: {
231
+ retry: false,
232
+ },
233
+ },
234
+ });
235
+ }
236
+
237
+ function Wrapper({ children }: PropsWithChildren) {
238
+ return (
239
+ <QueryClientProvider client={createQueryClient()}>
240
+ {children}
241
+ </QueryClientProvider>
242
+ );
243
+ }
244
+
245
+ function renderSubscriptionInvoice(
246
+ props: Partial<ComponentProps<typeof SubscriptionInvoice>> = {}
247
+ ) {
248
+ return render(
249
+ <SubscriptionInvoice
250
+ wsId="ws-1"
251
+ createMultipleInvoices={false}
252
+ defaultCategoryId="category-default"
253
+ defaultCurrency="VND"
254
+ defaultWalletId="wallet-default"
255
+ workspaceTimezone="Asia/Ho_Chi_Minh"
256
+ {...props}
257
+ />,
258
+ { wrapper: Wrapper }
259
+ );
260
+ }
261
+
262
+ describe('SubscriptionInvoice checkout defaults', () => {
263
+ beforeEach(() => {
264
+ testState.InvoicePaymentSettings.mockClear();
265
+ testState.useCategories.mockClear();
266
+ testState.useWallets.mockClear();
267
+ testState.productSelectionInjected = false;
268
+ testState.products[0]!.finance_category_id = null;
269
+ testState.wallets = [];
270
+ testState.categories = [];
271
+ });
272
+
273
+ it('preloads payment options from defaults and keeps default IDs while options load', async () => {
274
+ const { rerender } = renderSubscriptionInvoice();
275
+
276
+ expect(testState.useWallets).toHaveBeenCalledWith('ws-1', {
277
+ enabled: true,
278
+ });
279
+ expect(testState.useCategories).toHaveBeenCalledWith('ws-1', {
280
+ enabled: true,
281
+ });
282
+
283
+ await waitFor(() =>
284
+ expect(testState.InvoicePaymentSettings).toHaveBeenLastCalledWith(
285
+ expect.objectContaining({
286
+ categories: [],
287
+ selectedCategoryId: 'category-default',
288
+ selectedWalletId: 'wallet-default',
289
+ wallets: [],
290
+ })
291
+ )
292
+ );
293
+
294
+ testState.wallets = [
295
+ {
296
+ currency: 'VND',
297
+ id: 'wallet-default',
298
+ name: 'Default wallet',
299
+ type: 'STANDARD',
300
+ },
301
+ ];
302
+ testState.categories = [
303
+ {
304
+ id: 'category-default',
305
+ name: 'Default category',
306
+ },
307
+ ];
308
+
309
+ rerender(
310
+ <SubscriptionInvoice
311
+ wsId="ws-1"
312
+ createMultipleInvoices={false}
313
+ defaultCategoryId="category-default"
314
+ defaultCurrency="VND"
315
+ defaultWalletId="wallet-default"
316
+ workspaceTimezone="Asia/Ho_Chi_Minh"
317
+ />
318
+ );
319
+
320
+ await waitFor(() =>
321
+ expect(testState.InvoicePaymentSettings).toHaveBeenLastCalledWith(
322
+ expect.objectContaining({
323
+ categories: testState.categories,
324
+ selectedCategoryId: 'category-default',
325
+ selectedWalletId: 'wallet-default',
326
+ wallets: testState.wallets,
327
+ })
328
+ )
329
+ );
330
+ });
331
+ });