av6-core 1.0.19 → 1.0.21

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/index.d.mts CHANGED
@@ -266,6 +266,87 @@ declare const getPattern: {
266
266
  };
267
267
  declare const interpolate: (template: string, vars: Record<string, unknown>) => string;
268
268
 
269
+ type DiscountMode = "PERCENTAGE" | "AMOUNT";
270
+ type AdditionalDiscountMode = "PERCENTAGE" | "AMOUNT";
271
+ type CoPayType = "PERCENTAGE" | "AMOUNT";
272
+ type TaxMethod = "NONE" | "INCLUSIVE" | "EXCLUSIVE";
273
+ type CoPayMode = "EXCLUSIVE-INCLUSIVE" | "PERCENTAGE-AMOUNT";
274
+ declare const RoundFormat: {
275
+ readonly ROUND: "ROUND";
276
+ readonly SPECIAL_ROUND: "SPECIAL_ROUND";
277
+ readonly TO_FIXED: "TO_FIXED";
278
+ readonly CEIL: "CEIL";
279
+ readonly FLOOR: "FLOOR";
280
+ readonly TRUNC: "TRUNC";
281
+ readonly NONE: "NONE";
282
+ };
283
+ type RoundFormat = (typeof RoundFormat)[keyof typeof RoundFormat];
284
+ interface ChildCalcInput {
285
+ qty: number;
286
+ rate: number;
287
+ otherCharge?: number;
288
+ addonPercentage?: number;
289
+ discountMode?: DiscountMode;
290
+ discountValue?: number;
291
+ taxMethod?: TaxMethod;
292
+ taxValue?: number;
293
+ /** Absolute insurer-covered at line level */
294
+ coPaymentType?: CoPayType;
295
+ coPayValue?: number;
296
+ coPayMethod?: "EXCLUSIVE" | "INCLUSIVE";
297
+ }
298
+ interface MasterAdditionalDiscount {
299
+ mode: AdditionalDiscountMode;
300
+ value: number;
301
+ coPayMode?: CoPayMode;
302
+ }
303
+ interface CalcOptions {
304
+ calculationMethod?: "STEP_WISE" | "FINAL_ONLY";
305
+ lineRound?: RoundFormat;
306
+ headerRound?: RoundFormat;
307
+ precision?: number;
308
+ }
309
+ interface ChildCalculated {
310
+ baseRate: number;
311
+ subtotalAmount: number;
312
+ otherChargeAmount: number;
313
+ discountMode: DiscountMode;
314
+ discountValue: number;
315
+ discountAmount: number;
316
+ taxMethod: TaxMethod;
317
+ taxValue: number;
318
+ taxAmount: number;
319
+ grossAmount: number;
320
+ netAmount: number;
321
+ roundOffAmount: number;
322
+ copayAmount: number;
323
+ }
324
+ interface MasterCalculated {
325
+ additionalDiscountMode: AdditionalDiscountMode;
326
+ additionalDiscountValue: number;
327
+ subtotalAmount: number;
328
+ otherChargeAmount: number;
329
+ discountTotalAmount: number;
330
+ taxAmount: number;
331
+ grossAmount: number;
332
+ netAmount: number;
333
+ roundOffAmount: number;
334
+ copayAmount: number;
335
+ }
336
+ interface BillingCalcResult {
337
+ master: MasterCalculated;
338
+ children: ChildCalculated[];
339
+ }
340
+
341
+ /**
342
+ * calculateBillingFromChildren (master-level additional discount applied AFTER child calculations)
343
+ * - Child: item discount -> tax (INCLUSIVE/EXCLUSIVE/NONE) -> coPay -> patient line net + line rounding
344
+ * - Master: sum children, compute patientRawTotal = gross - Σcopay
345
+ * apply master additional discount ON patientRawTotal (no propagation to children)
346
+ * then header rounding
347
+ */
348
+ declare function calculateBillingFromChildren(inputs: ChildCalcInput[], masterExtra: MasterAdditionalDiscount, options?: CalcOptions): BillingCalcResult;
349
+
269
350
  interface CreateTransaction {
270
351
  field: string;
271
352
  changedFrom?: string | null;
@@ -385,4 +466,4 @@ declare class NotificationEmitter {
385
466
  notifyNow(eventName: string, body: Omit<EmitPayload, "eventName">): Promise<void>;
386
467
  }
387
468
 
388
- export { type CacheAdapter, type CalculationRes, type ColValue, type CommonExcelRequest, type CommonFilterRequest, type CommonServiceResponse, type Config, type Context, type CreateUINConfigRequest, type DataType, type DeleteParams, type DeleteRequestRepository, type Deps, type DropdownRequest, type DropdownRequestService, type DynamicShortCode, type EmitPayload, type ExcelConfig, type ExportExcel, type ExportExcelRequestService, type FetchRequest, type FetchRequestRepository, type FixedSearchRequest, type FixedSearchRequestService, type Helpers, type ImportExcel, type ImportExcelRequestService, type Mapper, type NewFixedSearchRequest, type NewFixedSearchRequestService, type NewSearchRequest, NotificationEmitter, type PaginatedResponse, type Recipient, type SearchRequest, type SearchRequestService, type ServiceCacheAdapter, type Store, type ToggleActive, type UINConfigDTO, type UINPreviewRequest, type UINSegment, type UINSegmentType, type UIN_RESET_POLICY, type UinDeps, type UpdateStatusRequestRepository, type UpdateUINConfigRequest, type ValidationErrorItem, commonService, customOmit, findDifferences, getDynamicValue, getPattern, interpolate, objectTo2DArray, toNumberOrNull, toUINConfigDTO, uinConfigService, type updateStatusParams };
469
+ export { type AdditionalDiscountMode, type BillingCalcResult, type CacheAdapter, type CalcOptions, type CalculationRes, type ChildCalcInput, type ChildCalculated, type CoPayMode, type CoPayType, type ColValue, type CommonExcelRequest, type CommonFilterRequest, type CommonServiceResponse, type Config, type Context, type CreateUINConfigRequest, type DataType, type DeleteParams, type DeleteRequestRepository, type Deps, type DiscountMode, type DropdownRequest, type DropdownRequestService, type DynamicShortCode, type EmitPayload, type ExcelConfig, type ExportExcel, type ExportExcelRequestService, type FetchRequest, type FetchRequestRepository, type FixedSearchRequest, type FixedSearchRequestService, type Helpers, type ImportExcel, type ImportExcelRequestService, type Mapper, type MasterAdditionalDiscount, type MasterCalculated, type NewFixedSearchRequest, type NewFixedSearchRequestService, type NewSearchRequest, NotificationEmitter, type PaginatedResponse, type Recipient, RoundFormat, type SearchRequest, type SearchRequestService, type ServiceCacheAdapter, type Store, type TaxMethod, type ToggleActive, type UINConfigDTO, type UINPreviewRequest, type UINSegment, type UINSegmentType, type UIN_RESET_POLICY, type UinDeps, type UpdateStatusRequestRepository, type UpdateUINConfigRequest, type ValidationErrorItem, calculateBillingFromChildren, commonService, customOmit, findDifferences, getDynamicValue, getPattern, interpolate, objectTo2DArray, toNumberOrNull, toUINConfigDTO, uinConfigService, type updateStatusParams };
package/dist/index.d.ts CHANGED
@@ -266,6 +266,87 @@ declare const getPattern: {
266
266
  };
267
267
  declare const interpolate: (template: string, vars: Record<string, unknown>) => string;
268
268
 
269
+ type DiscountMode = "PERCENTAGE" | "AMOUNT";
270
+ type AdditionalDiscountMode = "PERCENTAGE" | "AMOUNT";
271
+ type CoPayType = "PERCENTAGE" | "AMOUNT";
272
+ type TaxMethod = "NONE" | "INCLUSIVE" | "EXCLUSIVE";
273
+ type CoPayMode = "EXCLUSIVE-INCLUSIVE" | "PERCENTAGE-AMOUNT";
274
+ declare const RoundFormat: {
275
+ readonly ROUND: "ROUND";
276
+ readonly SPECIAL_ROUND: "SPECIAL_ROUND";
277
+ readonly TO_FIXED: "TO_FIXED";
278
+ readonly CEIL: "CEIL";
279
+ readonly FLOOR: "FLOOR";
280
+ readonly TRUNC: "TRUNC";
281
+ readonly NONE: "NONE";
282
+ };
283
+ type RoundFormat = (typeof RoundFormat)[keyof typeof RoundFormat];
284
+ interface ChildCalcInput {
285
+ qty: number;
286
+ rate: number;
287
+ otherCharge?: number;
288
+ addonPercentage?: number;
289
+ discountMode?: DiscountMode;
290
+ discountValue?: number;
291
+ taxMethod?: TaxMethod;
292
+ taxValue?: number;
293
+ /** Absolute insurer-covered at line level */
294
+ coPaymentType?: CoPayType;
295
+ coPayValue?: number;
296
+ coPayMethod?: "EXCLUSIVE" | "INCLUSIVE";
297
+ }
298
+ interface MasterAdditionalDiscount {
299
+ mode: AdditionalDiscountMode;
300
+ value: number;
301
+ coPayMode?: CoPayMode;
302
+ }
303
+ interface CalcOptions {
304
+ calculationMethod?: "STEP_WISE" | "FINAL_ONLY";
305
+ lineRound?: RoundFormat;
306
+ headerRound?: RoundFormat;
307
+ precision?: number;
308
+ }
309
+ interface ChildCalculated {
310
+ baseRate: number;
311
+ subtotalAmount: number;
312
+ otherChargeAmount: number;
313
+ discountMode: DiscountMode;
314
+ discountValue: number;
315
+ discountAmount: number;
316
+ taxMethod: TaxMethod;
317
+ taxValue: number;
318
+ taxAmount: number;
319
+ grossAmount: number;
320
+ netAmount: number;
321
+ roundOffAmount: number;
322
+ copayAmount: number;
323
+ }
324
+ interface MasterCalculated {
325
+ additionalDiscountMode: AdditionalDiscountMode;
326
+ additionalDiscountValue: number;
327
+ subtotalAmount: number;
328
+ otherChargeAmount: number;
329
+ discountTotalAmount: number;
330
+ taxAmount: number;
331
+ grossAmount: number;
332
+ netAmount: number;
333
+ roundOffAmount: number;
334
+ copayAmount: number;
335
+ }
336
+ interface BillingCalcResult {
337
+ master: MasterCalculated;
338
+ children: ChildCalculated[];
339
+ }
340
+
341
+ /**
342
+ * calculateBillingFromChildren (master-level additional discount applied AFTER child calculations)
343
+ * - Child: item discount -> tax (INCLUSIVE/EXCLUSIVE/NONE) -> coPay -> patient line net + line rounding
344
+ * - Master: sum children, compute patientRawTotal = gross - Σcopay
345
+ * apply master additional discount ON patientRawTotal (no propagation to children)
346
+ * then header rounding
347
+ */
348
+ declare function calculateBillingFromChildren(inputs: ChildCalcInput[], masterExtra: MasterAdditionalDiscount, options?: CalcOptions): BillingCalcResult;
349
+
269
350
  interface CreateTransaction {
270
351
  field: string;
271
352
  changedFrom?: string | null;
@@ -385,4 +466,4 @@ declare class NotificationEmitter {
385
466
  notifyNow(eventName: string, body: Omit<EmitPayload, "eventName">): Promise<void>;
386
467
  }
387
468
 
388
- export { type CacheAdapter, type CalculationRes, type ColValue, type CommonExcelRequest, type CommonFilterRequest, type CommonServiceResponse, type Config, type Context, type CreateUINConfigRequest, type DataType, type DeleteParams, type DeleteRequestRepository, type Deps, type DropdownRequest, type DropdownRequestService, type DynamicShortCode, type EmitPayload, type ExcelConfig, type ExportExcel, type ExportExcelRequestService, type FetchRequest, type FetchRequestRepository, type FixedSearchRequest, type FixedSearchRequestService, type Helpers, type ImportExcel, type ImportExcelRequestService, type Mapper, type NewFixedSearchRequest, type NewFixedSearchRequestService, type NewSearchRequest, NotificationEmitter, type PaginatedResponse, type Recipient, type SearchRequest, type SearchRequestService, type ServiceCacheAdapter, type Store, type ToggleActive, type UINConfigDTO, type UINPreviewRequest, type UINSegment, type UINSegmentType, type UIN_RESET_POLICY, type UinDeps, type UpdateStatusRequestRepository, type UpdateUINConfigRequest, type ValidationErrorItem, commonService, customOmit, findDifferences, getDynamicValue, getPattern, interpolate, objectTo2DArray, toNumberOrNull, toUINConfigDTO, uinConfigService, type updateStatusParams };
469
+ export { type AdditionalDiscountMode, type BillingCalcResult, type CacheAdapter, type CalcOptions, type CalculationRes, type ChildCalcInput, type ChildCalculated, type CoPayMode, type CoPayType, type ColValue, type CommonExcelRequest, type CommonFilterRequest, type CommonServiceResponse, type Config, type Context, type CreateUINConfigRequest, type DataType, type DeleteParams, type DeleteRequestRepository, type Deps, type DiscountMode, type DropdownRequest, type DropdownRequestService, type DynamicShortCode, type EmitPayload, type ExcelConfig, type ExportExcel, type ExportExcelRequestService, type FetchRequest, type FetchRequestRepository, type FixedSearchRequest, type FixedSearchRequestService, type Helpers, type ImportExcel, type ImportExcelRequestService, type Mapper, type MasterAdditionalDiscount, type MasterCalculated, type NewFixedSearchRequest, type NewFixedSearchRequestService, type NewSearchRequest, NotificationEmitter, type PaginatedResponse, type Recipient, RoundFormat, type SearchRequest, type SearchRequestService, type ServiceCacheAdapter, type Store, type TaxMethod, type ToggleActive, type UINConfigDTO, type UINPreviewRequest, type UINSegment, type UINSegmentType, type UIN_RESET_POLICY, type UinDeps, type UpdateStatusRequestRepository, type UpdateUINConfigRequest, type ValidationErrorItem, calculateBillingFromChildren, commonService, customOmit, findDifferences, getDynamicValue, getPattern, interpolate, objectTo2DArray, toNumberOrNull, toUINConfigDTO, uinConfigService, type updateStatusParams };
package/dist/index.js CHANGED
@@ -31,6 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  NotificationEmitter: () => NotificationEmitter,
34
+ RoundFormat: () => RoundFormat,
35
+ calculateBillingFromChildren: () => calculateBillingFromChildren,
34
36
  commonService: () => commonService,
35
37
  customOmit: () => customOmit,
36
38
  findDifferences: () => findDifferences,
@@ -1094,6 +1096,149 @@ var commonService = (serviceDeps) => {
1094
1096
  };
1095
1097
  };
1096
1098
 
1099
+ // src/types/calculation.ts
1100
+ var RoundFormat = {
1101
+ ROUND: "ROUND",
1102
+ SPECIAL_ROUND: "SPECIAL_ROUND",
1103
+ TO_FIXED: "TO_FIXED",
1104
+ CEIL: "CEIL",
1105
+ FLOOR: "FLOOR",
1106
+ TRUNC: "TRUNC",
1107
+ NONE: "NONE"
1108
+ };
1109
+
1110
+ // src/utils/calculation.utils.ts
1111
+ function applyRound(value, format, precision = 2) {
1112
+ switch (format) {
1113
+ case RoundFormat.NONE:
1114
+ return value;
1115
+ case RoundFormat.ROUND:
1116
+ return Math.round(value);
1117
+ case RoundFormat.SPECIAL_ROUND:
1118
+ return value < 1 ? Math.ceil(value) : Math.round(value);
1119
+ case RoundFormat.CEIL:
1120
+ return Math.ceil(value);
1121
+ case RoundFormat.FLOOR:
1122
+ return Math.floor(value);
1123
+ case RoundFormat.TRUNC:
1124
+ return Math.trunc(value);
1125
+ case RoundFormat.TO_FIXED:
1126
+ default:
1127
+ return Number(value.toFixed(Math.max(0, precision | 0)));
1128
+ }
1129
+ }
1130
+ var maybeStep = (v, stepwise, fmt, p) => stepwise ? applyRound(v, fmt, p) : v;
1131
+ function clamp(n, min, max) {
1132
+ return Math.max(min, Math.min(max, n));
1133
+ }
1134
+ function calculateBillingFromChildren(inputs, masterExtra, options = {}) {
1135
+ const stepwise = options.calculationMethod === "STEP_WISE";
1136
+ const lineFmt = options.lineRound ?? RoundFormat.TO_FIXED;
1137
+ const headFmt = options.headerRound ?? lineFmt;
1138
+ const precision = options.precision ?? 2;
1139
+ const children = inputs.map((it) => {
1140
+ let baseRate = it.rate;
1141
+ if (it.addonPercentage) {
1142
+ baseRate = baseRate + baseRate * (it.addonPercentage / 100);
1143
+ baseRate = maybeStep(baseRate, stepwise, lineFmt, precision);
1144
+ }
1145
+ const subtotal = applyRound(it.qty * baseRate, lineFmt, precision);
1146
+ const other = applyRound(it.otherCharge ?? 0, lineFmt, precision);
1147
+ const basePreTax = applyRound(subtotal + other, lineFmt, precision);
1148
+ let copayAmount2 = 0;
1149
+ if (masterExtra.coPayMode === "PERCENTAGE-AMOUNT") {
1150
+ const copayType = it.coPaymentType ?? "AMOUNT";
1151
+ const copayValue = it.coPayValue ?? 0;
1152
+ copayAmount2 = copayType === "PERCENTAGE" ? basePreTax * (copayValue / 100) : Math.min(copayValue, basePreTax);
1153
+ }
1154
+ const dMode = it.discountMode ?? "AMOUNT";
1155
+ const dVal = it.discountValue ?? 0;
1156
+ const tMethod = it.taxMethod ?? "NONE";
1157
+ const tVal = Math.max(0, it.taxValue ?? 0);
1158
+ let baseAfterTaxDisc = 0;
1159
+ let discountValue = 0;
1160
+ if (tMethod === "INCLUSIVE") {
1161
+ const inclusiveTaxMultiplier = (100 + tVal) / 100;
1162
+ baseAfterTaxDisc = basePreTax / inclusiveTaxMultiplier;
1163
+ }
1164
+ baseAfterTaxDisc = maybeStep(baseAfterTaxDisc, stepwise, lineFmt, precision);
1165
+ if (dMode === "AMOUNT") {
1166
+ discountValue = dVal;
1167
+ } else if (dMode === "PERCENTAGE") {
1168
+ discountValue = baseAfterTaxDisc * dVal / 100;
1169
+ }
1170
+ discountValue = maybeStep(discountValue, stepwise, lineFmt, precision);
1171
+ let afterDisc = Math.max(0, baseAfterTaxDisc - discountValue);
1172
+ afterDisc = maybeStep(afterDisc, stepwise, lineFmt, precision);
1173
+ let calculatedTax = tVal * afterDisc / 100;
1174
+ calculatedTax = maybeStep(calculatedTax, stepwise, lineFmt, precision);
1175
+ let grossAmount2 = afterDisc + calculatedTax;
1176
+ grossAmount2 = maybeStep(grossAmount2, stepwise, lineFmt, precision);
1177
+ if (masterExtra.coPayMode === "EXCLUSIVE-INCLUSIVE" && it.coPayMethod === "INCLUSIVE") {
1178
+ copayAmount2 = grossAmount2;
1179
+ }
1180
+ copayAmount2 = applyRound(copayAmount2, lineFmt, precision);
1181
+ const copay = clamp(Math.max(0, copayAmount2), 0, grossAmount2);
1182
+ const patientRaw = grossAmount2 - copay;
1183
+ const patientRounded = applyRound(patientRaw, lineFmt, precision);
1184
+ const lineRoundOff = patientRounded - patientRaw;
1185
+ return {
1186
+ baseRate: applyRound(baseRate, lineFmt, precision),
1187
+ subtotalAmount: subtotal,
1188
+ otherChargeAmount: other,
1189
+ discountMode: dMode,
1190
+ discountValue: dVal,
1191
+ discountAmount: applyRound(discountValue, lineFmt, precision),
1192
+ // item-only discount
1193
+ taxMethod: tMethod,
1194
+ taxValue: tVal,
1195
+ taxAmount: applyRound(calculatedTax, lineFmt, precision),
1196
+ grossAmount: applyRound(grossAmount2, lineFmt, precision),
1197
+ netAmount: patientRounded,
1198
+ // patient payable at line
1199
+ roundOffAmount: applyRound(lineRoundOff, "TO_FIXED", 2),
1200
+ // rounded - raw
1201
+ copayAmount: copay
1202
+ // insurer covered at line
1203
+ };
1204
+ });
1205
+ const subtotalAmount = children.reduce((a, c) => a + c.subtotalAmount, 0);
1206
+ const otherChargeAmount = children.reduce((a, c) => a + c.otherChargeAmount, 0);
1207
+ const itemDiscountSum = children.reduce((a, c) => a + c.discountAmount, 0);
1208
+ const taxAmount = children.reduce((a, c) => a + c.taxAmount, 0);
1209
+ const grossAmount = children.reduce((a, c) => a + c.grossAmount, 0);
1210
+ const copayAmount = children.reduce((a, c) => a + c.copayAmount, 0);
1211
+ const patientRawTotal = grossAmount - copayAmount;
1212
+ let masterExtraApplied = 0;
1213
+ if (masterExtra.mode === "PERCENTAGE") {
1214
+ masterExtraApplied = patientRawTotal * (masterExtra.value / 100);
1215
+ } else if (masterExtra.mode === "AMOUNT") {
1216
+ masterExtraApplied = Math.min(masterExtra.value, patientRawTotal);
1217
+ }
1218
+ masterExtraApplied = maybeStep(masterExtraApplied, stepwise, headFmt, precision);
1219
+ const discountTotalAmount = maybeStep(itemDiscountSum + masterExtraApplied, stepwise, headFmt, precision);
1220
+ const patientAfterExtra = Math.max(0, patientRawTotal - masterExtraApplied);
1221
+ const netAmount = applyRound(patientAfterExtra, headFmt, precision);
1222
+ const roundOffAmount = netAmount - patientAfterExtra;
1223
+ const master = {
1224
+ additionalDiscountMode: masterExtra.mode,
1225
+ additionalDiscountValue: masterExtra.value,
1226
+ subtotalAmount: applyRound(subtotalAmount, headFmt, precision),
1227
+ otherChargeAmount: applyRound(otherChargeAmount, headFmt, precision),
1228
+ discountTotalAmount: applyRound(discountTotalAmount, headFmt, precision),
1229
+ // includes master extra
1230
+ taxAmount: applyRound(taxAmount, headFmt, precision),
1231
+ grossAmount: applyRound(grossAmount, headFmt, precision),
1232
+ netAmount,
1233
+ // patient payable after master extra + rounding
1234
+ roundOffAmount: applyRound(roundOffAmount, "TO_FIXED", 2),
1235
+ // rounded - raw
1236
+ copayAmount: applyRound(copayAmount, headFmt, precision)
1237
+ // Σ line copay (insurer covered)
1238
+ };
1239
+ return { master, children };
1240
+ }
1241
+
1097
1242
  // src/utils/audit.utils.ts
1098
1243
  function isValidDate(value) {
1099
1244
  if (value instanceof Date) {
@@ -1544,8 +1689,32 @@ var EmailProvider = class {
1544
1689
 
1545
1690
  // src/providers/sms.provider.ts
1546
1691
  var SmsProvider = class {
1692
+ constructor(logger = console, args) {
1693
+ this.logger = logger;
1694
+ this.args = args;
1695
+ this.logger.info(`[NotificationService] args for sms : ${JSON.stringify(args)}`);
1696
+ this.url = args.apiUrl || "https://web.nsemfua.com/api/http/sms/send";
1697
+ this.token = "13|R3XfH6wHx3zdflBzpGMlNiTWKuOUEkGutORyGcuS";
1698
+ this.sender = args.senderId || "AlmaMedLab";
1699
+ }
1700
+ url;
1701
+ token;
1702
+ sender;
1547
1703
  async send(args) {
1548
- if (!args.recipient.phone) {
1704
+ const to = args.recipient;
1705
+ if (!to) {
1706
+ return {
1707
+ ok: false,
1708
+ provider: "SMS" /* SMS */,
1709
+ error: "Missing recipient.phone"
1710
+ };
1711
+ }
1712
+ const allRecipients = [to];
1713
+ if (this.args.masterNumber) {
1714
+ allRecipients.push({ phone: this.args.masterNumber });
1715
+ }
1716
+ const recipients = this.normalizeRecipients(allRecipients);
1717
+ if (!recipients.length) {
1549
1718
  return {
1550
1719
  ok: false,
1551
1720
  provider: "SMS" /* SMS */,
@@ -1553,11 +1722,41 @@ var SmsProvider = class {
1553
1722
  };
1554
1723
  }
1555
1724
  try {
1556
- const fake = { sid: "twilio-sid-123", status: "queued" };
1725
+ const payload = {
1726
+ api_token: this.token,
1727
+ recipient: recipients.join(", "),
1728
+ // API accepts comma-separated list
1729
+ sender_id: this.sender,
1730
+ type: "plain",
1731
+ message: this.cleanMessage(args.body)
1732
+ // schedule_time: "" // add if you need scheduling
1733
+ };
1734
+ const res = await fetch(this.url, {
1735
+ method: "POST",
1736
+ headers: {
1737
+ "Content-Type": "application/json",
1738
+ Accept: "application/json"
1739
+ },
1740
+ body: JSON.stringify(payload)
1741
+ });
1742
+ const data = await res.json().catch(() => ({}));
1743
+ const isError = data?.status === "error" || !res.ok;
1744
+ if (isError) {
1745
+ return {
1746
+ ok: false,
1747
+ provider: "SMS" /* SMS */,
1748
+ error: data?.message || data?.error || `Gateway error (${res.status} ${res.statusText})`
1749
+ };
1750
+ }
1751
+ const externalId = data?.id || data?.data?.id || data?.message_id || data?.data?.message_id || void 0;
1557
1752
  return {
1558
1753
  ok: true,
1559
1754
  provider: "SMS" /* SMS */,
1560
- externalId: fake.sid
1755
+ externalId,
1756
+ meta: {
1757
+ gatewayStatus: data?.status,
1758
+ recipient: payload.recipient
1759
+ }
1561
1760
  };
1562
1761
  } catch (err) {
1563
1762
  return {
@@ -1567,6 +1766,23 @@ var SmsProvider = class {
1567
1766
  };
1568
1767
  }
1569
1768
  }
1769
+ /** Accepts single/multiple Recipient objects and returns sanitized numbers */
1770
+ normalizeRecipients(input) {
1771
+ return input.map((r) => r?.phone || "").map((p) => this.stripCountryCode(p)).map((p) => this.onlyDialable(p)).filter(Boolean);
1772
+ }
1773
+ /** Match PHP: remove leading +233 (Ghana) only */
1774
+ stripCountryCode(phone) {
1775
+ return (phone || "").trim().replace(this.args.countryCode || "+233", "");
1776
+ }
1777
+ /** Remove spaces, dashes, parentheses; keep numbers only (and commas handled elsewhere) */
1778
+ onlyDialable(phone) {
1779
+ return phone.replace(/[^\d]/g, "");
1780
+ }
1781
+ /** Mirror PHP cleanup (strip tags, collapse nbsp, trim) */
1782
+ cleanMessage(msg) {
1783
+ const noTags = msg.replace(/<\/?[^>]+(>|$)/g, "");
1784
+ return noTags.replace(/&nbsp;|&amp;nbsp;/g, " ").trim();
1785
+ }
1570
1786
  };
1571
1787
 
1572
1788
  // src/providers/whatsapp.provider.ts
@@ -1703,7 +1919,11 @@ var NotificationService = class {
1703
1919
  });
1704
1920
  }
1705
1921
  const emailProvider = new EmailProvider(this.prisma, this.logger, this.envMode);
1706
- const smsProvider = new SmsProvider();
1922
+ const smsProvider = new SmsProvider(this.logger, {
1923
+ apiUrl: cfg.serviceEvent.smsApiUrl,
1924
+ countryCode: cfg.serviceEvent.countryCode,
1925
+ senderId: cfg.serviceEvent.smsSenderId
1926
+ });
1707
1927
  const waProvider = new WhatsAppProvider(
1708
1928
  {
1709
1929
  apiUrl: cfg.serviceEvent.wpApiUrl,
@@ -1882,6 +2102,8 @@ var NotificationEmitter = class {
1882
2102
  // Annotate the CommonJS export names for ESM import in node:
1883
2103
  0 && (module.exports = {
1884
2104
  NotificationEmitter,
2105
+ RoundFormat,
2106
+ calculateBillingFromChildren,
1885
2107
  commonService,
1886
2108
  customOmit,
1887
2109
  findDifferences,
package/dist/index.mjs CHANGED
@@ -1048,6 +1048,149 @@ var commonService = (serviceDeps) => {
1048
1048
  };
1049
1049
  };
1050
1050
 
1051
+ // src/types/calculation.ts
1052
+ var RoundFormat = {
1053
+ ROUND: "ROUND",
1054
+ SPECIAL_ROUND: "SPECIAL_ROUND",
1055
+ TO_FIXED: "TO_FIXED",
1056
+ CEIL: "CEIL",
1057
+ FLOOR: "FLOOR",
1058
+ TRUNC: "TRUNC",
1059
+ NONE: "NONE"
1060
+ };
1061
+
1062
+ // src/utils/calculation.utils.ts
1063
+ function applyRound(value, format, precision = 2) {
1064
+ switch (format) {
1065
+ case RoundFormat.NONE:
1066
+ return value;
1067
+ case RoundFormat.ROUND:
1068
+ return Math.round(value);
1069
+ case RoundFormat.SPECIAL_ROUND:
1070
+ return value < 1 ? Math.ceil(value) : Math.round(value);
1071
+ case RoundFormat.CEIL:
1072
+ return Math.ceil(value);
1073
+ case RoundFormat.FLOOR:
1074
+ return Math.floor(value);
1075
+ case RoundFormat.TRUNC:
1076
+ return Math.trunc(value);
1077
+ case RoundFormat.TO_FIXED:
1078
+ default:
1079
+ return Number(value.toFixed(Math.max(0, precision | 0)));
1080
+ }
1081
+ }
1082
+ var maybeStep = (v, stepwise, fmt, p) => stepwise ? applyRound(v, fmt, p) : v;
1083
+ function clamp(n, min, max) {
1084
+ return Math.max(min, Math.min(max, n));
1085
+ }
1086
+ function calculateBillingFromChildren(inputs, masterExtra, options = {}) {
1087
+ const stepwise = options.calculationMethod === "STEP_WISE";
1088
+ const lineFmt = options.lineRound ?? RoundFormat.TO_FIXED;
1089
+ const headFmt = options.headerRound ?? lineFmt;
1090
+ const precision = options.precision ?? 2;
1091
+ const children = inputs.map((it) => {
1092
+ let baseRate = it.rate;
1093
+ if (it.addonPercentage) {
1094
+ baseRate = baseRate + baseRate * (it.addonPercentage / 100);
1095
+ baseRate = maybeStep(baseRate, stepwise, lineFmt, precision);
1096
+ }
1097
+ const subtotal = applyRound(it.qty * baseRate, lineFmt, precision);
1098
+ const other = applyRound(it.otherCharge ?? 0, lineFmt, precision);
1099
+ const basePreTax = applyRound(subtotal + other, lineFmt, precision);
1100
+ let copayAmount2 = 0;
1101
+ if (masterExtra.coPayMode === "PERCENTAGE-AMOUNT") {
1102
+ const copayType = it.coPaymentType ?? "AMOUNT";
1103
+ const copayValue = it.coPayValue ?? 0;
1104
+ copayAmount2 = copayType === "PERCENTAGE" ? basePreTax * (copayValue / 100) : Math.min(copayValue, basePreTax);
1105
+ }
1106
+ const dMode = it.discountMode ?? "AMOUNT";
1107
+ const dVal = it.discountValue ?? 0;
1108
+ const tMethod = it.taxMethod ?? "NONE";
1109
+ const tVal = Math.max(0, it.taxValue ?? 0);
1110
+ let baseAfterTaxDisc = 0;
1111
+ let discountValue = 0;
1112
+ if (tMethod === "INCLUSIVE") {
1113
+ const inclusiveTaxMultiplier = (100 + tVal) / 100;
1114
+ baseAfterTaxDisc = basePreTax / inclusiveTaxMultiplier;
1115
+ }
1116
+ baseAfterTaxDisc = maybeStep(baseAfterTaxDisc, stepwise, lineFmt, precision);
1117
+ if (dMode === "AMOUNT") {
1118
+ discountValue = dVal;
1119
+ } else if (dMode === "PERCENTAGE") {
1120
+ discountValue = baseAfterTaxDisc * dVal / 100;
1121
+ }
1122
+ discountValue = maybeStep(discountValue, stepwise, lineFmt, precision);
1123
+ let afterDisc = Math.max(0, baseAfterTaxDisc - discountValue);
1124
+ afterDisc = maybeStep(afterDisc, stepwise, lineFmt, precision);
1125
+ let calculatedTax = tVal * afterDisc / 100;
1126
+ calculatedTax = maybeStep(calculatedTax, stepwise, lineFmt, precision);
1127
+ let grossAmount2 = afterDisc + calculatedTax;
1128
+ grossAmount2 = maybeStep(grossAmount2, stepwise, lineFmt, precision);
1129
+ if (masterExtra.coPayMode === "EXCLUSIVE-INCLUSIVE" && it.coPayMethod === "INCLUSIVE") {
1130
+ copayAmount2 = grossAmount2;
1131
+ }
1132
+ copayAmount2 = applyRound(copayAmount2, lineFmt, precision);
1133
+ const copay = clamp(Math.max(0, copayAmount2), 0, grossAmount2);
1134
+ const patientRaw = grossAmount2 - copay;
1135
+ const patientRounded = applyRound(patientRaw, lineFmt, precision);
1136
+ const lineRoundOff = patientRounded - patientRaw;
1137
+ return {
1138
+ baseRate: applyRound(baseRate, lineFmt, precision),
1139
+ subtotalAmount: subtotal,
1140
+ otherChargeAmount: other,
1141
+ discountMode: dMode,
1142
+ discountValue: dVal,
1143
+ discountAmount: applyRound(discountValue, lineFmt, precision),
1144
+ // item-only discount
1145
+ taxMethod: tMethod,
1146
+ taxValue: tVal,
1147
+ taxAmount: applyRound(calculatedTax, lineFmt, precision),
1148
+ grossAmount: applyRound(grossAmount2, lineFmt, precision),
1149
+ netAmount: patientRounded,
1150
+ // patient payable at line
1151
+ roundOffAmount: applyRound(lineRoundOff, "TO_FIXED", 2),
1152
+ // rounded - raw
1153
+ copayAmount: copay
1154
+ // insurer covered at line
1155
+ };
1156
+ });
1157
+ const subtotalAmount = children.reduce((a, c) => a + c.subtotalAmount, 0);
1158
+ const otherChargeAmount = children.reduce((a, c) => a + c.otherChargeAmount, 0);
1159
+ const itemDiscountSum = children.reduce((a, c) => a + c.discountAmount, 0);
1160
+ const taxAmount = children.reduce((a, c) => a + c.taxAmount, 0);
1161
+ const grossAmount = children.reduce((a, c) => a + c.grossAmount, 0);
1162
+ const copayAmount = children.reduce((a, c) => a + c.copayAmount, 0);
1163
+ const patientRawTotal = grossAmount - copayAmount;
1164
+ let masterExtraApplied = 0;
1165
+ if (masterExtra.mode === "PERCENTAGE") {
1166
+ masterExtraApplied = patientRawTotal * (masterExtra.value / 100);
1167
+ } else if (masterExtra.mode === "AMOUNT") {
1168
+ masterExtraApplied = Math.min(masterExtra.value, patientRawTotal);
1169
+ }
1170
+ masterExtraApplied = maybeStep(masterExtraApplied, stepwise, headFmt, precision);
1171
+ const discountTotalAmount = maybeStep(itemDiscountSum + masterExtraApplied, stepwise, headFmt, precision);
1172
+ const patientAfterExtra = Math.max(0, patientRawTotal - masterExtraApplied);
1173
+ const netAmount = applyRound(patientAfterExtra, headFmt, precision);
1174
+ const roundOffAmount = netAmount - patientAfterExtra;
1175
+ const master = {
1176
+ additionalDiscountMode: masterExtra.mode,
1177
+ additionalDiscountValue: masterExtra.value,
1178
+ subtotalAmount: applyRound(subtotalAmount, headFmt, precision),
1179
+ otherChargeAmount: applyRound(otherChargeAmount, headFmt, precision),
1180
+ discountTotalAmount: applyRound(discountTotalAmount, headFmt, precision),
1181
+ // includes master extra
1182
+ taxAmount: applyRound(taxAmount, headFmt, precision),
1183
+ grossAmount: applyRound(grossAmount, headFmt, precision),
1184
+ netAmount,
1185
+ // patient payable after master extra + rounding
1186
+ roundOffAmount: applyRound(roundOffAmount, "TO_FIXED", 2),
1187
+ // rounded - raw
1188
+ copayAmount: applyRound(copayAmount, headFmt, precision)
1189
+ // Σ line copay (insurer covered)
1190
+ };
1191
+ return { master, children };
1192
+ }
1193
+
1051
1194
  // src/utils/audit.utils.ts
1052
1195
  function isValidDate(value) {
1053
1196
  if (value instanceof Date) {
@@ -1498,8 +1641,32 @@ var EmailProvider = class {
1498
1641
 
1499
1642
  // src/providers/sms.provider.ts
1500
1643
  var SmsProvider = class {
1644
+ constructor(logger = console, args) {
1645
+ this.logger = logger;
1646
+ this.args = args;
1647
+ this.logger.info(`[NotificationService] args for sms : ${JSON.stringify(args)}`);
1648
+ this.url = args.apiUrl || "https://web.nsemfua.com/api/http/sms/send";
1649
+ this.token = "13|R3XfH6wHx3zdflBzpGMlNiTWKuOUEkGutORyGcuS";
1650
+ this.sender = args.senderId || "AlmaMedLab";
1651
+ }
1652
+ url;
1653
+ token;
1654
+ sender;
1501
1655
  async send(args) {
1502
- if (!args.recipient.phone) {
1656
+ const to = args.recipient;
1657
+ if (!to) {
1658
+ return {
1659
+ ok: false,
1660
+ provider: "SMS" /* SMS */,
1661
+ error: "Missing recipient.phone"
1662
+ };
1663
+ }
1664
+ const allRecipients = [to];
1665
+ if (this.args.masterNumber) {
1666
+ allRecipients.push({ phone: this.args.masterNumber });
1667
+ }
1668
+ const recipients = this.normalizeRecipients(allRecipients);
1669
+ if (!recipients.length) {
1503
1670
  return {
1504
1671
  ok: false,
1505
1672
  provider: "SMS" /* SMS */,
@@ -1507,11 +1674,41 @@ var SmsProvider = class {
1507
1674
  };
1508
1675
  }
1509
1676
  try {
1510
- const fake = { sid: "twilio-sid-123", status: "queued" };
1677
+ const payload = {
1678
+ api_token: this.token,
1679
+ recipient: recipients.join(", "),
1680
+ // API accepts comma-separated list
1681
+ sender_id: this.sender,
1682
+ type: "plain",
1683
+ message: this.cleanMessage(args.body)
1684
+ // schedule_time: "" // add if you need scheduling
1685
+ };
1686
+ const res = await fetch(this.url, {
1687
+ method: "POST",
1688
+ headers: {
1689
+ "Content-Type": "application/json",
1690
+ Accept: "application/json"
1691
+ },
1692
+ body: JSON.stringify(payload)
1693
+ });
1694
+ const data = await res.json().catch(() => ({}));
1695
+ const isError = data?.status === "error" || !res.ok;
1696
+ if (isError) {
1697
+ return {
1698
+ ok: false,
1699
+ provider: "SMS" /* SMS */,
1700
+ error: data?.message || data?.error || `Gateway error (${res.status} ${res.statusText})`
1701
+ };
1702
+ }
1703
+ const externalId = data?.id || data?.data?.id || data?.message_id || data?.data?.message_id || void 0;
1511
1704
  return {
1512
1705
  ok: true,
1513
1706
  provider: "SMS" /* SMS */,
1514
- externalId: fake.sid
1707
+ externalId,
1708
+ meta: {
1709
+ gatewayStatus: data?.status,
1710
+ recipient: payload.recipient
1711
+ }
1515
1712
  };
1516
1713
  } catch (err) {
1517
1714
  return {
@@ -1521,6 +1718,23 @@ var SmsProvider = class {
1521
1718
  };
1522
1719
  }
1523
1720
  }
1721
+ /** Accepts single/multiple Recipient objects and returns sanitized numbers */
1722
+ normalizeRecipients(input) {
1723
+ return input.map((r) => r?.phone || "").map((p) => this.stripCountryCode(p)).map((p) => this.onlyDialable(p)).filter(Boolean);
1724
+ }
1725
+ /** Match PHP: remove leading +233 (Ghana) only */
1726
+ stripCountryCode(phone) {
1727
+ return (phone || "").trim().replace(this.args.countryCode || "+233", "");
1728
+ }
1729
+ /** Remove spaces, dashes, parentheses; keep numbers only (and commas handled elsewhere) */
1730
+ onlyDialable(phone) {
1731
+ return phone.replace(/[^\d]/g, "");
1732
+ }
1733
+ /** Mirror PHP cleanup (strip tags, collapse nbsp, trim) */
1734
+ cleanMessage(msg) {
1735
+ const noTags = msg.replace(/<\/?[^>]+(>|$)/g, "");
1736
+ return noTags.replace(/&nbsp;|&amp;nbsp;/g, " ").trim();
1737
+ }
1524
1738
  };
1525
1739
 
1526
1740
  // src/providers/whatsapp.provider.ts
@@ -1657,7 +1871,11 @@ var NotificationService = class {
1657
1871
  });
1658
1872
  }
1659
1873
  const emailProvider = new EmailProvider(this.prisma, this.logger, this.envMode);
1660
- const smsProvider = new SmsProvider();
1874
+ const smsProvider = new SmsProvider(this.logger, {
1875
+ apiUrl: cfg.serviceEvent.smsApiUrl,
1876
+ countryCode: cfg.serviceEvent.countryCode,
1877
+ senderId: cfg.serviceEvent.smsSenderId
1878
+ });
1661
1879
  const waProvider = new WhatsAppProvider(
1662
1880
  {
1663
1881
  apiUrl: cfg.serviceEvent.wpApiUrl,
@@ -1835,6 +2053,8 @@ var NotificationEmitter = class {
1835
2053
  };
1836
2054
  export {
1837
2055
  NotificationEmitter,
2056
+ RoundFormat,
2057
+ calculateBillingFromChildren,
1838
2058
  commonService,
1839
2059
  customOmit,
1840
2060
  findDifferences,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "av6-core",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",