@openmrs/esm-billing-app 1.0.2-pre.800 → 1.0.2-pre.802

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.
@@ -71,9 +71,9 @@
71
71
  "initial": false,
72
72
  "entry": false,
73
73
  "recorded": false,
74
- "size": 1172991,
74
+ "size": 1173266,
75
75
  "sizes": {
76
- "javascript": 1172949,
76
+ "javascript": 1173224,
77
77
  "consume-shared": 42
78
78
  },
79
79
  "names": [],
@@ -87,7 +87,7 @@
87
87
  "auxiliaryFiles": [
88
88
  "942.js.map"
89
89
  ],
90
- "hash": "7b6c48d2956841c8",
90
+ "hash": "3f477f3def97306b",
91
91
  "childrenByOrder": {}
92
92
  },
93
93
  {
@@ -544,9 +544,9 @@
544
544
  "initial": false,
545
545
  "entry": false,
546
546
  "recorded": false,
547
- "size": 8850,
547
+ "size": 8777,
548
548
  "sizes": {
549
- "javascript": 8850
549
+ "javascript": 8777
550
550
  },
551
551
  "names": [],
552
552
  "idHints": [],
@@ -558,7 +558,7 @@
558
558
  "4300.js"
559
559
  ],
560
560
  "auxiliaryFiles": [],
561
- "hash": "4e1e7e3ab3cd456c",
561
+ "hash": "5e3aefcd437e10e9",
562
562
  "childrenByOrder": {}
563
563
  },
564
564
  {
@@ -1209,10 +1209,10 @@
1209
1209
  "initial": true,
1210
1210
  "entry": true,
1211
1211
  "recorded": false,
1212
- "size": 5470821,
1212
+ "size": 5471096,
1213
1213
  "sizes": {
1214
1214
  "consume-shared": 210,
1215
- "javascript": 5448166,
1215
+ "javascript": 5448441,
1216
1216
  "share-init": 336,
1217
1217
  "runtime": 22109
1218
1218
  },
@@ -1229,7 +1229,7 @@
1229
1229
  "auxiliaryFiles": [
1230
1230
  "main.js.map"
1231
1231
  ],
1232
- "hash": "d33a3b534cb9fac4",
1232
+ "hash": "0ffc610a1e0163be",
1233
1233
  "childrenByOrder": {}
1234
1234
  },
1235
1235
  {
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.24.0","fhir2":">=1.2"},"pages":[{"component":"billableServicesHome","route":"billable-services"}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"},"featureFlag":"billing"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing history"},"featureFlag":"billing"},{"name":"billable-services-app-menu-item","component":"billableServicesAppMenuItem","slot":"app-menu-item-slot","meta":{"name":"Billable Services"}},{"name":"billing-checkin-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm","featureFlag":"billing"},{"slot":"system-admin-page-card-link-slot","component":"billableServicesCardLink","name":"billable-services-admin-card-link"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"billing-home-tiles-ext","slot":"billing-home-tiles-slot","component":"serviceMetrics"},{"name":"edit-bill-line-item-dialog","component":"editBillLineItemModal","online":true,"offline":true}],"modals":[{"name":"add-cash-point-modal","component":"addCashPointModal"},{"name":"add-payment-mode-modal","component":"addPaymentModeModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"edit-bill-item-modal","component":"editBillLineItemModal"},{"name":"edit-billable-service-modal","component":"editBillableServiceModal"},{"name":"require-billing-modal","component":"requirePaymentModal"}],"workspaces":[{"name":"billing-form-workspace","title":"billingForm","component":"billingFormWorkspace","type":"form"}],"featureFlags":[{"flagName":"billing","label":"Billing module","description":"This feature introduces navigation links on the patient chart and home page to allow accessing the billing module features"}],"version":"1.0.2-pre.800"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.24.0","fhir2":">=1.2"},"pages":[{"component":"billableServicesHome","route":"billable-services"}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"},"featureFlag":"billing"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing history"},"featureFlag":"billing"},{"name":"billable-services-app-menu-item","component":"billableServicesAppMenuItem","slot":"app-menu-item-slot","meta":{"name":"Billable Services"}},{"name":"billing-checkin-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm","featureFlag":"billing"},{"slot":"system-admin-page-card-link-slot","component":"billableServicesCardLink","name":"billable-services-admin-card-link"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"billing-home-tiles-ext","slot":"billing-home-tiles-slot","component":"serviceMetrics"},{"name":"edit-bill-line-item-dialog","component":"editBillLineItemModal","online":true,"offline":true}],"modals":[{"name":"add-cash-point-modal","component":"addCashPointModal"},{"name":"add-payment-mode-modal","component":"addPaymentModeModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"edit-bill-item-modal","component":"editBillLineItemModal"},{"name":"edit-billable-service-modal","component":"editBillableServiceModal"},{"name":"require-billing-modal","component":"requirePaymentModal"}],"workspaces":[{"name":"billing-form-workspace","title":"billingForm","component":"billingFormWorkspace","type":"form"}],"featureFlags":[{"flagName":"billing","label":"Billing module","description":"This feature introduces navigation links on the patient chart and home page to allow accessing the billing module features"}],"version":"1.0.2-pre.802"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-billing-app",
3
- "version": "1.0.2-pre.800",
3
+ "version": "1.0.2-pre.802",
4
4
  "description": "O3 frontend module for handling billing concerns in healthcare settings",
5
5
  "browser": "dist/openmrs-esm-billing-app.js",
6
6
  "main": "src/index.ts",
@@ -65,19 +65,23 @@ const createBillableServiceSchema = (t: TFunction) => {
65
65
  })
66
66
  .trim()
67
67
  .min(1, t('paymentModeRequired', 'Payment mode is required')),
68
- price: z.union([
69
- z.number().positive(t('priceMustBeGreaterThanZero', 'Price must be greater than 0')),
70
- z
71
- .string({
72
- required_error: t('priceIsRequired', 'Price is required'),
73
- })
74
- .trim()
75
- .min(1, t('priceIsRequired', 'Price is required'))
76
- .refine(
77
- (val) => !isNaN(parseFloat(val)) && parseFloat(val) > 0,
78
- t('priceMustBeValidPositiveNumber', 'Price must be a valid positive number'),
79
- ),
80
- ]),
68
+ price: z.union([z.number(), z.string(), z.undefined()]).superRefine((val, ctx) => {
69
+ if (val === undefined || val === null || val === '') {
70
+ ctx.addIssue({
71
+ code: z.ZodIssueCode.custom,
72
+ message: t('priceIsRequired', 'Price is required'),
73
+ });
74
+ return;
75
+ }
76
+
77
+ const numValue = typeof val === 'number' ? val : parseFloat(val);
78
+ if (isNaN(numValue) || numValue <= 0) {
79
+ ctx.addIssue({
80
+ code: z.ZodIssueCode.custom,
81
+ message: t('priceMustBeGreaterThanZero', 'Price must be greater than 0'),
82
+ });
83
+ }
84
+ }),
81
85
  });
82
86
 
83
87
  return z.object({
@@ -52,64 +52,75 @@ const mockServiceTypes = [
52
52
  { uuid: 'a487a743-62ce-4f93-a66b-c5154ee8987d', display: 'Adherence counselling service' },
53
53
  ];
54
54
 
55
+ // Test helpers (canonical pattern)
56
+ const setupMocks = () => {
57
+ mockUseBillableServices.mockReturnValue({
58
+ billableServices: [],
59
+ isLoading: false,
60
+ error: null,
61
+ mutate: jest.fn(),
62
+ isValidating: false,
63
+ });
64
+ mockUsePaymentModes.mockReturnValue({ paymentModes: mockPaymentModes, error: null, isLoadingPaymentModes: false });
65
+ mockUseServiceTypes.mockReturnValue({ serviceTypes: mockServiceTypes, error: false, isLoadingServiceTypes: false });
66
+ mockUseConceptsSearch.mockReturnValue({ searchResults: [], isSearching: false, error: null });
67
+ };
68
+
69
+ const renderAddBillableService = (props = {}) => {
70
+ const defaultProps = {
71
+ onClose: jest.fn(),
72
+ ...props,
73
+ };
74
+ setupMocks();
75
+ return render(<AddBillableService {...defaultProps} />);
76
+ };
77
+
78
+ interface FillOptions {
79
+ serviceName?: string;
80
+ shortName?: string;
81
+ skipPrice?: boolean;
82
+ }
83
+
84
+ const fillRequiredFields = async (user, options: FillOptions = {}) => {
85
+ const { serviceName = 'Test Service Name', shortName = 'Test Short Name', skipPrice = false } = options;
86
+
87
+ if (serviceName) {
88
+ await user.type(screen.getByRole('textbox', { name: /Service Name/i }), serviceName);
89
+ }
90
+ if (shortName) {
91
+ await user.type(screen.getByRole('textbox', { name: /Short Name/i }), shortName);
92
+ }
93
+
94
+ await user.click(screen.getByRole('combobox', { name: /Service type/i }));
95
+ await user.click(screen.getByRole('option', { name: /Lab service/i }));
96
+
97
+ await user.click(screen.getByRole('combobox', { name: /Payment mode/i }));
98
+ await user.click(screen.getByRole('option', { name: /Cash/i }));
99
+
100
+ if (!skipPrice) {
101
+ const priceInput = screen.getByRole('textbox', { name: /Selling Price/i });
102
+ await user.type(priceInput, '100');
103
+ }
104
+ };
105
+
106
+ const submitForm = async (user) => {
107
+ const saveBtn = screen.getByRole('button', { name: /save/i });
108
+ await user.click(saveBtn);
109
+ };
110
+
55
111
  describe('AddBillableService', () => {
56
112
  test('should render billable services form and generate correct payload', async () => {
57
113
  const user = userEvent.setup();
58
114
  const mockOnClose = jest.fn();
59
- mockUseBillableServices.mockReturnValue({
60
- billableServices: [],
61
- isLoading: false,
62
- error: null,
63
- mutate: jest.fn(),
64
- isValidating: false,
65
- });
66
- mockUsePaymentModes.mockReturnValue({ paymentModes: mockPaymentModes, error: null, isLoadingPaymentModes: false });
67
- mockUseServiceTypes.mockReturnValue({ serviceTypes: mockServiceTypes, error: false, isLoadingServiceTypes: false });
68
- mockUseConceptsSearch.mockReturnValue({ searchResults: [], isSearching: false, error: null });
69
-
70
- render(<AddBillableService onClose={mockOnClose} />);
115
+ renderAddBillableService({ onClose: mockOnClose });
71
116
 
72
117
  const formTitle = screen.getByRole('heading', { name: /Add Billable Services/i });
73
118
  expect(formTitle).toBeInTheDocument();
74
119
 
75
- const serviceNameTextInp = screen.getByRole('textbox', { name: /Service Name/i });
76
- expect(serviceNameTextInp).toBeInTheDocument();
77
-
78
- const serviceShortNameTextInp = screen.getByRole('textbox', { name: /Short Name/i });
79
- expect(serviceShortNameTextInp).toBeInTheDocument();
80
-
81
- await user.type(serviceNameTextInp, 'Test Service Name');
82
- await user.type(serviceShortNameTextInp, 'Test Short Name');
83
-
84
- expect(serviceNameTextInp).toHaveValue('Test Service Name');
85
- expect(serviceShortNameTextInp).toHaveValue('Test Short Name');
86
-
87
- const serviceTypeComboBox = screen.getByRole('combobox', { name: /Service type/i });
88
- expect(serviceTypeComboBox).toBeInTheDocument();
89
- await user.click(serviceTypeComboBox);
90
- const serviceTypeOptions = screen.getByRole('option', { name: /Lab service/i });
91
- expect(serviceTypeOptions).toBeInTheDocument();
92
- await user.click(serviceTypeOptions);
93
-
94
- // Fill in the default payment option (first one)
95
- const paymentMethodComboBoxes = screen.getAllByRole('combobox', { name: /Payment mode/i });
96
- expect(paymentMethodComboBoxes).toHaveLength(1); // Should have one default
97
- await user.click(paymentMethodComboBoxes[0]);
98
- const paymentMethodOptions = screen.getByRole('option', { name: /Cash/i });
99
- expect(paymentMethodOptions).toBeInTheDocument();
100
- await user.click(paymentMethodOptions);
120
+ await fillRequiredFields(user);
121
+ mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
101
122
 
102
- const priceTextInps = screen.getAllByRole('textbox', { name: /Selling Price/i });
103
- expect(priceTextInps).toHaveLength(1); // Should have one price input for the default payment method
104
- const priceTextInp = priceTextInps[0];
105
- expect(priceTextInp).toBeInTheDocument();
106
- await user.type(priceTextInp, '1000');
107
-
108
- mockCreateBillableService.mockReturnValue(Promise.resolve({} as FetchResponse<any>));
109
- const saveBtn = screen.getAllByRole('button').find((btn) => btn.getAttribute('type') === 'submit');
110
- expect(saveBtn).toBeInTheDocument();
111
-
112
- await user.click(saveBtn);
123
+ await submitForm(user);
113
124
 
114
125
  expect(mockCreateBillableService).toHaveBeenCalledTimes(1);
115
126
  expect(mockCreateBillableService).toHaveBeenCalledWith({
@@ -119,7 +130,7 @@ describe('AddBillableService', () => {
119
130
  servicePrices: [
120
131
  {
121
132
  paymentMode: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74',
122
- price: 1000,
133
+ price: 100,
123
134
  name: 'Cash',
124
135
  },
125
136
  ],
@@ -133,23 +144,97 @@ describe('AddBillableService', () => {
133
144
  test("should navigate back to billable services dashboard when 'Cancel' button is clicked", async () => {
134
145
  const user = userEvent.setup();
135
146
  const mockOnClose = jest.fn();
136
- mockUseBillableServices.mockReturnValue({
137
- billableServices: [],
138
- isLoading: false,
139
- error: null,
140
- mutate: jest.fn(),
141
- isValidating: false,
142
- });
143
- mockUsePaymentModes.mockReturnValue({ paymentModes: mockPaymentModes, error: null, isLoadingPaymentModes: false });
144
- mockUseServiceTypes.mockReturnValue({ serviceTypes: mockServiceTypes, error: false, isLoadingServiceTypes: false });
145
- mockUseConceptsSearch.mockReturnValue({ searchResults: [], isSearching: false, error: null });
147
+ renderAddBillableService({ onClose: mockOnClose });
146
148
 
147
- render(<AddBillableService onClose={mockOnClose} />);
148
-
149
- const cancelBtn = screen.getAllByRole('button').find((btn) => btn.className.includes('cds--btn--secondary'));
150
- expect(cancelBtn).toBeInTheDocument();
149
+ const cancelBtn = screen.getByRole('button', { name: /cancel/i });
151
150
  await user.click(cancelBtn);
152
151
 
153
152
  expect(mockOnClose).toHaveBeenCalledTimes(1);
154
153
  });
154
+
155
+ describe('Form Validation', () => {
156
+ test('should show "Price must be greater than 0" error for zero price', async () => {
157
+ const user = userEvent.setup();
158
+ renderAddBillableService();
159
+
160
+ await fillRequiredFields(user, { skipPrice: true });
161
+
162
+ const priceInput = screen.getByRole('textbox', { name: /selling price/i });
163
+ await user.type(priceInput, '0');
164
+
165
+ await submitForm(user);
166
+
167
+ expect(screen.getByText('Price must be greater than 0')).toBeInTheDocument();
168
+ expect(mockCreateBillableService).not.toHaveBeenCalled();
169
+ });
170
+
171
+ test('should show "Price must be greater than 0" error for negative price', async () => {
172
+ const user = userEvent.setup();
173
+ renderAddBillableService();
174
+
175
+ await fillRequiredFields(user, { skipPrice: true });
176
+
177
+ const priceInput = screen.getByRole('textbox', { name: /Selling Price/i });
178
+ await user.type(priceInput, '-10');
179
+
180
+ await submitForm(user);
181
+
182
+ expect(screen.getByText('Price must be greater than 0')).toBeInTheDocument();
183
+ expect(mockCreateBillableService).not.toHaveBeenCalled();
184
+ });
185
+
186
+ test('should show "Service name is required" error when service name is empty', async () => {
187
+ const user = userEvent.setup();
188
+ renderAddBillableService();
189
+
190
+ // Fill all fields except service name
191
+ await user.type(screen.getByRole('textbox', { name: /Short Name/i }), 'Test Short Name');
192
+
193
+ await user.click(screen.getByRole('combobox', { name: /Service type/i }));
194
+ await user.click(screen.getByRole('option', { name: /Lab service/i }));
195
+
196
+ await user.click(screen.getByRole('combobox', { name: /Payment mode/i }));
197
+ await user.click(screen.getByRole('option', { name: /Cash/i }));
198
+
199
+ const priceInput = screen.getByRole('textbox', { name: /Selling Price/i });
200
+ await user.type(priceInput, '100');
201
+
202
+ await submitForm(user);
203
+
204
+ expect(screen.getByText('Service name is required')).toBeInTheDocument();
205
+ expect(mockCreateBillableService).not.toHaveBeenCalled();
206
+ });
207
+
208
+ test('should accept valid decimal price values', async () => {
209
+ const user = userEvent.setup();
210
+ renderAddBillableService();
211
+
212
+ await fillRequiredFields(user, { skipPrice: true });
213
+
214
+ const priceInput = screen.getByRole('textbox', { name: /Selling Price/i });
215
+ await user.type(priceInput, '10.50');
216
+
217
+ mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
218
+
219
+ await submitForm(user);
220
+
221
+ expect(screen.queryByText('Price is required')).not.toBeInTheDocument();
222
+ expect(screen.queryByText('Price must be greater than 0')).not.toBeInTheDocument();
223
+ expect(mockCreateBillableService).toHaveBeenCalledTimes(1);
224
+ expect(mockCreateBillableService).toHaveBeenCalledWith({
225
+ name: 'Test Service Name',
226
+ shortName: 'Test Short Name',
227
+ serviceType: 'c9604249-db0a-4d03-b074-fc6bc2fa13e6',
228
+ servicePrices: [
229
+ {
230
+ paymentMode: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74',
231
+ price: 10.5,
232
+ name: 'Cash',
233
+ },
234
+ ],
235
+ serviceStatus: 'ENABLED',
236
+ concept: undefined,
237
+ });
238
+ });
239
+ });
155
240
  });
@@ -161,7 +161,6 @@
161
161
  "priceMustBeGreaterThanZero": "Price must be greater than 0",
162
162
  "priceMustBeNumber": "Price must be a valid number",
163
163
  "priceMustBePositive": "Price must be greater than 0",
164
- "priceMustBeValidPositiveNumber": "Price must be a valid positive number",
165
164
  "prices": "Prices",
166
165
  "printBill": "Print bill",
167
166
  "printReceipt": "Print receipt",