@kenyaemr/esm-billing-app 5.4.2-pre.2301 → 5.4.2-pre.2306

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/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","slot":"patient-chart-dashboard-slot","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"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","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"billingOverviewLink","name":"billing-overview-link","order":0,"slot":"billing-dashboard-group-nav-slot"},{"component":"billDepositDashboardLink","name":"bill-deposit-dashboard-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentHistoryLink","name":"payment-history-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentPointsLink","name":"payment-points-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentModesLink","name":"payment-modes-link","slot":"billing-dashboard-group-nav-slot"},{"component":"billManagerLink","name":"bill-manager-link","slot":"billing-dashboard-group-nav-slot"},{"component":"chargeableItemsLink","name":"chargeable-items-link","slot":"billing-dashboard-group-nav-slot"},{"component":"billableExemptionsLink","name":"billable-exemptions-link","slot":"billing-dashboard-group-nav-slot"},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","order":0,"slot":"claims-management-dashboard-link-slot"},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"},{"name":"add-deposit-workspace","component":"addDepositWorkspace","title":"Add Deposit","type":"other-form"},{"name":"deposit-transaction-workspace","component":"depositTransactionWorkspace","title":"Deposit Transaction","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"},{"name":"delete-deposit-modal","component":"deleteDepositModal"},{"name":"reverse-transaction-modal","component":"reverseTransactionModal"},{"name":"print-preview-modal","component":"printPreviewModal"},{"name":"bill-action-modal","component":"billActionModal"}],"version":"5.4.2-pre.2301"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","slot":"patient-chart-dashboard-slot","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"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","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"billingOverviewLink","name":"billing-overview-link","order":0,"slot":"billing-dashboard-group-nav-slot"},{"component":"billDepositDashboardLink","name":"bill-deposit-dashboard-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentHistoryLink","name":"payment-history-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentPointsLink","name":"payment-points-link","slot":"billing-dashboard-group-nav-slot"},{"component":"paymentModesLink","name":"payment-modes-link","slot":"billing-dashboard-group-nav-slot"},{"component":"billManagerLink","name":"bill-manager-link","slot":"billing-dashboard-group-nav-slot"},{"component":"chargeableItemsLink","name":"chargeable-items-link","slot":"billing-dashboard-group-nav-slot"},{"component":"billableExemptionsLink","name":"billable-exemptions-link","slot":"billing-dashboard-group-nav-slot"},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","order":0,"slot":"claims-management-dashboard-link-slot"},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"},{"name":"add-deposit-workspace","component":"addDepositWorkspace","title":"Add Deposit","type":"other-form"},{"name":"deposit-transaction-workspace","component":"depositTransactionWorkspace","title":"Deposit Transaction","type":"other-form"},{"name":"payment-workspace","component":"paymentWorkspace","title":"Payment Workspace","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"},{"name":"delete-deposit-modal","component":"deleteDepositModal"},{"name":"reverse-transaction-modal","component":"reverseTransactionModal"},{"name":"print-preview-modal","component":"printPreviewModal"},{"name":"bill-action-modal","component":"billActionModal"}],"version":"5.4.2-pre.2306"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-billing-app",
3
- "version": "5.4.2-pre.2301",
3
+ "version": "5.4.2-pre.2306",
4
4
  "description": "Billing app for KenyaEMR",
5
5
  "browser": "dist/kenyaemr-esm-billing-app.js",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,7 @@
1
1
  import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
2
  import { type BillDeposit, type CreateDepositPayload } from '../types/bill-deposit.types';
3
3
  import { MAX_REFERENCE_NUMBER_COUNTER } from '../constants/bill-deposit.constants';
4
+ import { formatCurrencySimple } from '../../helpers/currency';
4
5
 
5
6
  /**
6
7
  * Generates a unique reference number for bill deposits
@@ -55,13 +56,10 @@ export const deleteDeposit = async (uuid: string) => {
55
56
  };
56
57
 
57
58
  /**
58
- * Formats a deposit amount with currency symbol
59
+ * Formats a deposit amount with currency symbol based on locale
59
60
  */
60
61
  export const formatDepositAmount = (amount: number): string => {
61
- return new Intl.NumberFormat('en-US', {
62
- style: 'currency',
63
- currency: 'KES',
64
- }).format(amount);
62
+ return formatCurrencySimple(amount);
65
63
  };
66
64
 
67
65
  /**
@@ -23,6 +23,7 @@ import {
23
23
 
24
24
  import { LineItem, MappedBill } from '../../../../types';
25
25
  import { processBillPayment } from '../../../../billing.resource';
26
+ import { formatCurrencySimple } from '../../../../helpers/currency';
26
27
  import styles from './edit-bill.scss';
27
28
  import { createEditBillPayload } from './edit-bill-util';
28
29
  import classNames from 'classnames';
@@ -93,10 +94,7 @@ export const EditBillForm: React.FC<EditBillFormProps> = ({
93
94
  return <InlineLoading description={t('loading', 'Loading')} />;
94
95
  }
95
96
 
96
- const formattedPrice = Intl.NumberFormat('en-US', {
97
- style: 'currency',
98
- currency: 'KES',
99
- }).format(lineItem.price);
97
+ const formattedPrice = formatCurrencySimple(lineItem.price);
100
98
 
101
99
  const subtitleText = `${t('currentPriceAndQuantity', 'Current price and quantity')}: ${t(
102
100
  'price',
@@ -291,3 +291,8 @@ export const billingFormSchema = z.object({
291
291
  )
292
292
  .min(1),
293
293
  });
294
+
295
+ export const addPaymentToBill = (billUuid: string, payload: Record<string, any>) => {
296
+ const url = `${restBaseUrl}/cashier/bill/${billUuid}/payment`;
297
+ return openmrsFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload });
298
+ };
@@ -14,7 +14,7 @@ import {
14
14
  ComboBox,
15
15
  } from '@carbon/react';
16
16
  import { zodResolver } from '@hookform/resolvers/zod';
17
- import { navigate, showSnackbar, useConfig, useSession } from '@openmrs/esm-framework';
17
+ import { formatDatetime, navigate, showSnackbar, useConfig, useSession } from '@openmrs/esm-framework';
18
18
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
19
19
  import { Controller, FormProvider, useForm } from 'react-hook-form';
20
20
  import { useTranslation } from 'react-i18next';
@@ -22,7 +22,6 @@ import { useParams } from 'react-router-dom';
22
22
  import { z } from 'zod';
23
23
  import SHABenefitPackangesAndInterventions from '../../../benefits-package/forms/packages-and-interventions-form.component';
24
24
  import { BillingConfig } from '../../../config-schema';
25
- import { formatDate } from '../../../helpers/functions';
26
25
  import { useSystemSetting } from '../../../hooks/getMflCode';
27
26
  import usePatientDiagnosis from '../../../hooks/usePatientDiagnosis';
28
27
  import useProvider from '../../../hooks/useProvider';
@@ -155,8 +154,8 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
155
154
  diagnoses: diagnoses?.map((d) => d.id) ?? [],
156
155
  visitType: recentVisit?.visitType?.display || '',
157
156
  facility: `${recentVisit?.location?.display || ''} - ${mflCodeValue || ''}`,
158
- treatmentStart: recentVisit?.startDatetime ? formatDate(recentVisit.startDatetime) : '',
159
- treatmentEnd: recentVisit?.stopDatetime ? formatDate(recentVisit.stopDatetime) : '',
157
+ treatmentStart: formatDatetime(new Date(recentVisit?.startDatetime || '')),
158
+ treatmentEnd: formatDatetime(new Date(recentVisit?.stopDatetime || '')),
160
159
  packages: packagesAndinterventions?.packages ?? [],
161
160
  interventions: packagesAndinterventions?.interventions ?? [],
162
161
  provider: providerUuid,
@@ -34,6 +34,7 @@ export interface BillingConfig {
34
34
  enable: boolean;
35
35
  duration: number;
36
36
  };
37
+ localeCurrencyMapping: Record<string, string>;
37
38
  }
38
39
 
39
40
  export const configSchema: ConfigSchema = {
@@ -207,4 +208,16 @@ export const configSchema: ConfigSchema = {
207
208
  duration: 24,
208
209
  },
209
210
  },
211
+ localeCurrencyMapping: {
212
+ _type: Type.Object,
213
+ _description: 'Mapping of locale codes to currency codes for internationalization',
214
+ _default: {
215
+ en: 'KES',
216
+ sw: 'KES',
217
+ am: 'ETB',
218
+ 'en-KE': 'KES',
219
+ 'sw-KE': 'KES',
220
+ 'am-ET': 'ETB',
221
+ },
222
+ },
210
223
  };
@@ -0,0 +1,174 @@
1
+ # Currency Localization
2
+
3
+ This module provides locale-based currency formatting for the billing application. It automatically detects the user's locale and formats currency amounts accordingly.
4
+
5
+ ## Features
6
+
7
+ - **Locale-based currency mapping**: Automatically maps locales to appropriate currencies
8
+ - **Configurable currency mapping**: Supports custom currency mappings via configuration
9
+ - **Consistent formatting**: Provides standardized currency formatting across the application
10
+ - **Fallback support**: Gracefully handles unknown locales with sensible defaults
11
+
12
+ ## Supported Locales and Currencies
13
+
14
+ | Locale | Currency | Description |
15
+ |--------|----------|-------------|
16
+ | `en` | KES | English - Kenyan Shilling |
17
+ | `sw` | KES | Swahili - Kenyan Shilling |
18
+ | `am` | ETB | Amharic - Ethiopian Birr |
19
+ | `en-KE` | KES | English Kenya |
20
+ | `sw-KE` | KES | Swahili Kenya |
21
+ | `am-ET` | ETB | Amharic Ethiopia |
22
+
23
+ ## Usage
24
+
25
+ ### Basic Currency Formatting
26
+
27
+ ```typescript
28
+ import { formatCurrency, formatCurrencySimple } from './helpers/currency';
29
+
30
+ // Format with negative sign handling
31
+ const amount = formatCurrency(1234.56); // Returns: "KSh 1,234.56"
32
+
33
+ // Format without negative sign handling
34
+ const simpleAmount = formatCurrencySimple(1234.56); // Returns: "KSh 1,234.56"
35
+ ```
36
+
37
+ ### Using the Hook (with config support)
38
+
39
+ ```typescript
40
+ import { useCurrencyFormatting } from './helpers/currency';
41
+
42
+ const MyComponent = () => {
43
+ const { format, formatSimple, getCurrency, getLocale } = useCurrencyFormatting();
44
+
45
+ const amount = format(1234.56);
46
+ const currency = getCurrency(); // Returns current currency code
47
+
48
+ return <div>Amount: {amount}</div>;
49
+ };
50
+ ```
51
+
52
+ ### Direct Currency/Locale Access
53
+
54
+ ```typescript
55
+ import { getCurrencyForLocale, getCurrentLocale } from './helpers/currency';
56
+
57
+ const currency = getCurrencyForLocale(); // Returns: "KES", "ETB", etc.
58
+ const locale = getCurrentLocale(); // Returns: "en", "sw", "am", etc.
59
+ ```
60
+
61
+ ## Configuration
62
+
63
+ You can customize the locale-to-currency mapping by adding a `localeCurrencyMapping` configuration to your billing config:
64
+
65
+ ```json
66
+ {
67
+ "localeCurrencyMapping": {
68
+ "en": "USD",
69
+ "sw": "KES",
70
+ "am": "ETB",
71
+ "fr": "EUR"
72
+ }
73
+ }
74
+ ```
75
+
76
+ ## Functions
77
+
78
+ ### `formatCurrency(amount, options?)`
79
+ Formats a number as currency with negative sign handling.
80
+
81
+ **Parameters:**
82
+ - `amount` (number): The amount to format
83
+ - `options` (Intl.NumberFormatOptions, optional): Additional formatting options
84
+
85
+ **Returns:** Formatted currency string
86
+
87
+ ### `formatCurrencySimple(amount, options?)`
88
+ Formats a number as currency without negative sign handling.
89
+
90
+ **Parameters:**
91
+ - `amount` (number): The amount to format
92
+ - `options` (Intl.NumberFormatOptions, optional): Additional formatting options
93
+
94
+ **Returns:** Formatted currency string
95
+
96
+ ### `getCurrencyForLocale()`
97
+ Gets the currency code for the current locale.
98
+
99
+ **Returns:** Currency code string (e.g., "KES", "ETB")
100
+
101
+ ### `getCurrentLocale()`
102
+ Gets the current locale from localStorage.
103
+
104
+ **Returns:** Locale string (e.g., "en", "sw", "am")
105
+
106
+ ### `useCurrencyFormatting()`
107
+ React hook that provides currency formatting with config support.
108
+
109
+ **Returns:** Object with formatting functions and utilities
110
+
111
+ ## Migration from Hardcoded Currency
112
+
113
+ If you have existing code using hardcoded currency formatting, replace it with the new utilities:
114
+
115
+ **Before:**
116
+ ```typescript
117
+ const formatter = new Intl.NumberFormat('en-US', {
118
+ style: 'currency',
119
+ currency: 'KES',
120
+ }).format(amount);
121
+ ```
122
+
123
+ **After:**
124
+ ```typescript
125
+ import { formatCurrency } from './helpers/currency';
126
+
127
+ const formattedAmount = formatCurrency(amount);
128
+ ```
129
+
130
+ ## Testing
131
+
132
+ Run the currency utility tests:
133
+
134
+ ```bash
135
+ npm test -- currency.test.ts
136
+ ```
137
+
138
+ ## Examples
139
+
140
+ ### In Components
141
+
142
+ ```typescript
143
+ import React from 'react';
144
+ import { formatCurrency } from './helpers/currency';
145
+
146
+ const BillItem = ({ price, quantity }) => {
147
+ const total = price * quantity;
148
+
149
+ return (
150
+ <div>
151
+ <span>Price: {formatCurrency(price)}</span>
152
+ <span>Total: {formatCurrency(total)}</span>
153
+ </div>
154
+ );
155
+ };
156
+ ```
157
+
158
+ ### In Utilities
159
+
160
+ ```typescript
161
+ import { formatCurrencySimple } from './helpers/currency';
162
+
163
+ export const createReceipt = (items) => {
164
+ const total = items.reduce((sum, item) => sum + item.price, 0);
165
+
166
+ return {
167
+ items: items.map(item => ({
168
+ ...item,
169
+ formattedPrice: formatCurrencySimple(item.price)
170
+ })),
171
+ total: formatCurrency(total)
172
+ };
173
+ };
174
+ ```
@@ -0,0 +1,114 @@
1
+ import {
2
+ DEFAULT_LOCALE_CURRENCY_MAP,
3
+ getCurrencyForLocale,
4
+ getCurrentLocale,
5
+ formatCurrency,
6
+ formatCurrencySimple,
7
+ } from './currency';
8
+
9
+ // Mock localStorage
10
+ const mockLocalStorage = {
11
+ getItem: jest.fn(),
12
+ setItem: jest.fn(),
13
+ removeItem: jest.fn(),
14
+ clear: jest.fn(),
15
+ };
16
+
17
+ Object.defineProperty(window, 'localStorage', {
18
+ value: mockLocalStorage,
19
+ });
20
+
21
+ describe('Currency Utilities', () => {
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ });
25
+
26
+ describe('DEFAULT_LOCALE_CURRENCY_MAP', () => {
27
+ it('should have correct currency mappings', () => {
28
+ expect(DEFAULT_LOCALE_CURRENCY_MAP.en).toBe('KES');
29
+ expect(DEFAULT_LOCALE_CURRENCY_MAP.sw).toBe('KES');
30
+ expect(DEFAULT_LOCALE_CURRENCY_MAP.am).toBe('ETB');
31
+ expect(DEFAULT_LOCALE_CURRENCY_MAP['en-KE']).toBe('KES');
32
+ expect(DEFAULT_LOCALE_CURRENCY_MAP['sw-KE']).toBe('KES');
33
+ expect(DEFAULT_LOCALE_CURRENCY_MAP['am-ET']).toBe('ETB');
34
+ });
35
+ });
36
+
37
+ describe('getCurrentLocale', () => {
38
+ it('should return locale from localStorage', () => {
39
+ mockLocalStorage.getItem.mockReturnValue('sw');
40
+ expect(getCurrentLocale()).toBe('sw');
41
+ });
42
+
43
+ it('should return default locale when localStorage is empty', () => {
44
+ mockLocalStorage.getItem.mockReturnValue(null);
45
+ expect(getCurrentLocale()).toBe('en');
46
+ });
47
+ });
48
+
49
+ describe('getCurrencyForLocale', () => {
50
+ it('should return correct currency for English locale', () => {
51
+ mockLocalStorage.getItem.mockReturnValue('en');
52
+ expect(getCurrencyForLocale()).toBe('KES');
53
+ });
54
+
55
+ it('should return correct currency for Swahili locale', () => {
56
+ mockLocalStorage.getItem.mockReturnValue('sw');
57
+ expect(getCurrencyForLocale()).toBe('KES');
58
+ });
59
+
60
+ it('should return correct currency for Amharic locale', () => {
61
+ mockLocalStorage.getItem.mockReturnValue('am');
62
+ expect(getCurrencyForLocale()).toBe('ETB');
63
+ });
64
+
65
+ it('should return KES as fallback for unknown locale', () => {
66
+ mockLocalStorage.getItem.mockReturnValue('fr');
67
+ expect(getCurrencyForLocale()).toBe('KES');
68
+ });
69
+ });
70
+
71
+ describe('formatCurrency', () => {
72
+ beforeEach(() => {
73
+ // Mock Intl.NumberFormat
74
+ const mockFormatter = {
75
+ format: jest.fn((value) => `$${value.toFixed(2)}`),
76
+ };
77
+ jest.spyOn(Intl, 'NumberFormat').mockImplementation(() => mockFormatter as any);
78
+ });
79
+
80
+ it('should format positive amounts correctly', () => {
81
+ mockLocalStorage.getItem.mockReturnValue('en');
82
+ const result = formatCurrency(1234.56);
83
+ expect(result).toBe('$1234.56');
84
+ });
85
+
86
+ it('should format negative amounts with minus sign', () => {
87
+ mockLocalStorage.getItem.mockReturnValue('en');
88
+ const result = formatCurrency(-1234.56);
89
+ expect(result).toBe('- $1234.56');
90
+ });
91
+ });
92
+
93
+ describe('formatCurrencySimple', () => {
94
+ beforeEach(() => {
95
+ // Mock Intl.NumberFormat
96
+ const mockFormatter = {
97
+ format: jest.fn((value) => `$${value.toFixed(2)}`),
98
+ };
99
+ jest.spyOn(Intl, 'NumberFormat').mockImplementation(() => mockFormatter as any);
100
+ });
101
+
102
+ it('should format amounts without negative sign handling', () => {
103
+ mockLocalStorage.getItem.mockReturnValue('en');
104
+ const result = formatCurrencySimple(1234.56);
105
+ expect(result).toBe('$1234.56');
106
+ });
107
+
108
+ it('should format negative amounts without special handling', () => {
109
+ mockLocalStorage.getItem.mockReturnValue('en');
110
+ const result = formatCurrencySimple(-1234.56);
111
+ expect(result).toBe('$-1234.56');
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,138 @@
1
+ import { useConfig } from '@openmrs/esm-framework';
2
+ import { BillingConfig } from '../config-schema';
3
+
4
+ // Default locale to currency mapping (fallback)
5
+ export const DEFAULT_LOCALE_CURRENCY_MAP: Record<string, string> = {
6
+ en: 'KES', // English - Kenyan Shilling
7
+ sw: 'KES', // Swahili - Kenyan Shilling
8
+ am: 'ETB', // Amharic - Ethiopian Birr
9
+ 'en-KE': 'KES', // English Kenya
10
+ 'sw-KE': 'KES', // Swahili Kenya
11
+ 'am-ET': 'ETB', // Amharic Ethiopia
12
+ };
13
+
14
+ /**
15
+ * Gets the currency code for the current locale
16
+ * @returns The currency code (e.g., 'KES', 'ETB')
17
+ */
18
+ export const getCurrencyForLocale = (): string => {
19
+ const currentLocale = localStorage.getItem('i18nextLng') ?? 'en';
20
+ return DEFAULT_LOCALE_CURRENCY_MAP[currentLocale] || 'KES';
21
+ };
22
+
23
+ /**
24
+ * Gets the currency code for the current locale with config support
25
+ * @param config - The billing configuration object
26
+ * @returns The currency code (e.g., 'KES', 'ETB')
27
+ */
28
+ export const getCurrencyForLocaleWithConfig = (config?: BillingConfig): string => {
29
+ const currentLocale = localStorage.getItem('i18nextLng') ?? 'en';
30
+
31
+ // Use config mapping if available, otherwise fall back to default
32
+ const currencyMap = config?.localeCurrencyMapping || DEFAULT_LOCALE_CURRENCY_MAP;
33
+ return currencyMap[currentLocale] || 'KES';
34
+ };
35
+
36
+ /**
37
+ * Gets the current locale from localStorage
38
+ * @returns The current locale (e.g., 'en', 'sw', 'am')
39
+ */
40
+ export const getCurrentLocale = (): string => {
41
+ return localStorage.getItem('i18nextLng') ?? 'en';
42
+ };
43
+
44
+ /**
45
+ * Formats a number as currency based on the current locale
46
+ * @param amount - The amount to format
47
+ * @param options - Additional Intl.NumberFormat options
48
+ * @returns Formatted currency string
49
+ */
50
+ export const formatCurrency = (amount: number, options: Intl.NumberFormatOptions = {}): string => {
51
+ const currentLocale = getCurrentLocale();
52
+ const currency = getCurrencyForLocale();
53
+
54
+ const formatter = new Intl.NumberFormat(currentLocale, {
55
+ style: 'currency',
56
+ currency: currency,
57
+ minimumFractionDigits: 2,
58
+ ...options,
59
+ });
60
+
61
+ let formattedAmount = formatter.format(Math.abs(amount));
62
+
63
+ if (amount < 0) {
64
+ formattedAmount = `- ${formattedAmount}`;
65
+ }
66
+
67
+ return formattedAmount;
68
+ };
69
+
70
+ /**
71
+ * Formats a number as currency without negative sign handling
72
+ * @param amount - The amount to format
73
+ * @param options - Additional Intl.NumberFormat options
74
+ * @returns Formatted currency string
75
+ */
76
+ export const formatCurrencySimple = (amount: number, options: Intl.NumberFormatOptions = {}): string => {
77
+ const currentLocale = getCurrentLocale();
78
+ const currency = getCurrencyForLocale();
79
+
80
+ const formatter = new Intl.NumberFormat(currentLocale, {
81
+ style: 'currency',
82
+ currency: currency,
83
+ ...options,
84
+ });
85
+
86
+ return formatter.format(amount);
87
+ };
88
+
89
+ /**
90
+ * Hook to get currency formatting with config support
91
+ * @returns Object with currency formatting functions
92
+ */
93
+ export const useCurrencyFormatting = () => {
94
+ const config = useConfig<BillingConfig>();
95
+
96
+ const getCurrency = () => getCurrencyForLocaleWithConfig(config);
97
+ const getLocale = () => getCurrentLocale();
98
+
99
+ const format = (amount: number, options: Intl.NumberFormatOptions = {}) => {
100
+ const currentLocale = getLocale();
101
+ const currency = getCurrency();
102
+
103
+ const formatter = new Intl.NumberFormat(currentLocale, {
104
+ style: 'currency',
105
+ currency: currency,
106
+ minimumFractionDigits: 2,
107
+ ...options,
108
+ });
109
+
110
+ let formattedAmount = formatter.format(Math.abs(amount));
111
+
112
+ if (amount < 0) {
113
+ formattedAmount = `- ${formattedAmount}`;
114
+ }
115
+
116
+ return formattedAmount;
117
+ };
118
+
119
+ const formatSimple = (amount: number, options: Intl.NumberFormatOptions = {}) => {
120
+ const currentLocale = getLocale();
121
+ const currency = getCurrency();
122
+
123
+ const formatter = new Intl.NumberFormat(currentLocale, {
124
+ style: 'currency',
125
+ currency: currency,
126
+ ...options,
127
+ });
128
+
129
+ return formatter.format(amount);
130
+ };
131
+
132
+ return {
133
+ getCurrency,
134
+ getLocale,
135
+ format,
136
+ formatSimple,
137
+ };
138
+ };
@@ -1,5 +1,6 @@
1
1
  import dayjs from 'dayjs';
2
2
  import { Payment, LineItem } from '../types';
3
+ import { formatCurrency } from './currency';
3
4
 
4
5
  // amount already paid
5
6
  export function calculateTotalAmountTendered(payments: Array<Payment>) {
@@ -33,19 +34,7 @@ export function calculateTotalAmount(lineItems: Array<LineItem>) {
33
34
  }
34
35
 
35
36
  export const convertToCurrency = (amountToConvert: number) => {
36
- const formatter = new Intl.NumberFormat('en-KE', {
37
- style: 'currency',
38
- currency: 'KES',
39
- minimumFractionDigits: 2,
40
- });
41
-
42
- let formattedAmount = formatter.format(Math.abs(amountToConvert));
43
-
44
- if (amountToConvert < 0) {
45
- formattedAmount = `- ${formattedAmount}`;
46
- }
47
-
48
- return formattedAmount;
37
+ return formatCurrency(amountToConvert);
49
38
  };
50
39
 
51
40
  export const getGender = (gender: string, t) => {
package/src/index.ts CHANGED
@@ -76,6 +76,7 @@ import DepositTransactionWorkspace from './bill-deposit/components/forms/deposit
76
76
 
77
77
  // Print Preview Components
78
78
  import PrintPreviewModal from './print-preview/print-preview.modal';
79
+ import PaymentWorkspace from './invoice/payments/payment-form/payment.workspace';
79
80
 
80
81
  // Translation
81
82
  export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
@@ -218,6 +219,7 @@ export const visitAttributeTags = getSyncLifecycle(VisitAttributeTags, options);
218
219
  export const initiatePaymentDialog = getSyncLifecycle(InitiatePaymentDialog, options);
219
220
  export const paymentModeWorkspace = getSyncLifecycle(PaymentModeWorkspace, options);
220
221
  export const deletePaymentModeModal = getSyncLifecycle(DeletePaymentModeModal, options);
222
+ export const paymentWorkspace = getSyncLifecycle(PaymentWorkspace, options);
221
223
 
222
224
  // Payment Points Components
223
225
  export const createPaymentPoint = getSyncLifecycle(CreatePaymentPoint, options);