tango-app-api-payment-subscription 3.4.3 → 3.5.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.
@@ -5,6 +5,7 @@ import * as invoiceService from '../services/invoice.service.js';
5
5
  import { updateOrder, verifySignature, verifyWebhook } from 'tango-app-api-middleware/src/utils/razorPay.js';
6
6
  import { aggregateTransaction, createTransaction } from '../services/transaction.service.js';
7
7
  import dayjs from 'dayjs';
8
+ import { symbolFor } from '../utils/currency.js';
8
9
 
9
10
 
10
11
  export const getVirtualAccount = async ( req, res ) => {
@@ -376,9 +377,9 @@ export const transactionList = async ( req, res ) => {
376
377
  exportResult.push( {
377
378
  'Billing date': transaction?.billingDate ? dayjs( transaction.billingDate ).format( 'DD MMM, YYYY' ) : '',
378
379
  'Transaction': transaction?.transactionType === 'debt' ? `Consumption for ${dayjs( transaction?.billingDate ).format( 'MMM YYYY' )}- ${transaction?.groupName}` : 'Paid Credit',
379
- 'Debit': transaction?.transactionType === 'debt' ? `${transaction?.currency === 'inr' ? '₹' : '$'} ${transaction.amount}` : '',
380
- 'Credit': transaction?.transactionType === 'credit' ? `${transaction?.currency === 'inr' ? '₹' : '$'} ${transaction.amount}` : '',
381
- 'Balance credit': `${transaction?.balanceCreditCurrency === 'inr' ? '₹' : '$'} ${transaction.balanceCredit}` || '',
380
+ 'Debit': transaction?.transactionType === 'debt' ? `${symbolFor( transaction?.currency )} ${transaction.amount}` : '',
381
+ 'Credit': transaction?.transactionType === 'credit' ? `${symbolFor( transaction?.currency )} ${transaction.amount}` : '',
382
+ 'Balance credit': `${symbolFor( transaction?.balanceCreditCurrency )} ${transaction.balanceCredit}` || '',
382
383
 
383
384
  } );
384
385
  }
@@ -13,6 +13,7 @@ import * as cameraService from '../services/camera.service.js';
13
13
  import * as billingService from '../services/billing.service.js';
14
14
  import * as paymentAccountService from '../services/paymentAccount.service.js';
15
15
  import * as taggingService from '../services/tagging.service.js';
16
+ import { symbolFor } from '../utils/currency.js';
16
17
 
17
18
 
18
19
  import dayjs from 'dayjs';
@@ -2417,7 +2418,9 @@ export const pricingListUpdate = async ( req, res ) => {
2417
2418
  amount = amount + item.price;
2418
2419
  subtotal = subtotal+item.originalPrice;
2419
2420
  origPrice = origPrice + ( item.basePrice * count );
2420
- item.price = item.originalPrice.toFixed( 2 );
2421
+ item.price = String( Math.round( item.originalPrice ) );
2422
+ item.originalPrice = Math.round( item.originalPrice );
2423
+ item.basePrice = Math.round( item.basePrice );
2421
2424
  } );
2422
2425
  let discountAmount = origPrice - amount;
2423
2426
  let discountPercentage = ( discountAmount / origPrice ) * 100;
@@ -2425,7 +2428,7 @@ export const pricingListUpdate = async ( req, res ) => {
2425
2428
  ...productList,
2426
2429
  amount: amount.toFixed( 2 ),
2427
2430
  subtotal: subtotal.toFixed( 2 ),
2428
- currencyType: req.body.client.paymentInvoice.currencyType == 'dollar' ? '$' : '₹',
2431
+ currencyType: symbolFor( req.body.client.paymentInvoice.currencyType ),
2429
2432
  total: ( parseFloat( subtotal ) + parseFloat( ( amount * GST ) / 100 ) ).toFixed( 2 ),
2430
2433
  final: amount.toFixed( 2 ),
2431
2434
  discount: `${discountPercentage.toFixed( 2 )}% (-${discountAmount.toFixed( 2 )})`,
@@ -2666,7 +2669,7 @@ export const updatedRevisedPrice = async ( req, res ) => {
2666
2669
  extendDays: clientDetails.paymentInvoice.extendPaymentPeriodDays,
2667
2670
  address: clientDetails.billingDetails.billingAddress,
2668
2671
  amount: amount.toFixed( 2 ),
2669
- currencyType: clientDetails.paymentInvoice.currencyType == 'dollar' ? '$' : '₹',
2672
+ currencyType: symbolFor( clientDetails.paymentInvoice.currencyType ),
2670
2673
  discount: `${req.body.discount.toFixed( 2 )}% (-${discountAmount.toFixed( 2 )})`,
2671
2674
  total: ( parseFloat( amount ) + parseFloat( ( amount * gst ) / 100 ) ).toFixed( 2 ),
2672
2675
  final: req.body.revisedAmount.toFixed( 2 ),
@@ -2975,9 +2978,9 @@ export const invoiceDownload = async ( req, res ) => {
2975
2978
 
2976
2979
  amount = amount + item.price;
2977
2980
 
2978
- item.basePrice = item.basePrice.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
2979
- item.price = item.price.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
2980
- item.currency = clientDetails?.paymentInvoice?.currencyType == 'dollar' ? '$' : '₹';
2981
+ item.basePrice = Math.round( item.basePrice ).toLocaleString( 'en-IN' );
2982
+ item.price = Math.round( item.price ).toLocaleString( 'en-IN' );
2983
+ item.currency = symbolFor( clientDetails?.paymentInvoice?.currencyType );
2981
2984
  } );
2982
2985
  for ( let tax of invoiceInfo.tax ) {
2983
2986
  tax.taxAmount = tax.taxAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } );
@@ -3001,7 +3004,7 @@ export const invoiceDownload = async ( req, res ) => {
3001
3004
  PoNum: '',
3002
3005
  amountwords: AmountinWords,
3003
3006
  Terms: `Term ${clientDetails.paymentInvoice.extendPaymentPeriodDays}`,
3004
- currencyType: clientDetails?.paymentInvoice?.currencyType == 'dollar' ? '$' : '₹',
3007
+ currencyType: symbolFor( clientDetails?.paymentInvoice?.currencyType ),
3005
3008
  totalAmount: invoiceInfo.totalAmount.toLocaleString( 'en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 } ),
3006
3009
  invoiceDate,
3007
3010
  dueDate,
@@ -3053,7 +3056,18 @@ export const invoiceDownload = async ( req, res ) => {
3053
3056
  return res.sendSuccess( result );
3054
3057
  }
3055
3058
  }
3056
- res.set( 'Content-Disposition', 'attachment; filename="generated-pdf.pdf"' );
3059
+ const invoiceMonth = invoiceInfo.billingDate ?
3060
+ dayjs( invoiceInfo.billingDate ).format( 'MMMM' ) :
3061
+ ( invoiceInfo.monthOfbilling ? dayjs( invoiceInfo.monthOfbilling, 'MM' ).format( 'MMMM' ) : '' );
3062
+ let groupForName;
3063
+ if ( invoiceInfo.groupId ) {
3064
+ groupForName = await billingService.findOne( { _id: invoiceInfo.groupId } );
3065
+ }
3066
+ const brandName = groupForName?.registeredCompanyName || invoiceInfo.companyName || '';
3067
+ const safeFilename = `${invoiceInfo.invoice}-${invoiceMonth}-${brandName}.pdf`
3068
+ .replace( /[\r\n"\\]/g, '' )
3069
+ .trim();
3070
+ res.set( 'Content-Disposition', `attachment; filename="${safeFilename}"` );
3057
3071
  res.set( 'Content-Type', 'application/pdf' );
3058
3072
  res.send( pdfBuffer );
3059
3073
  } );
@@ -3318,15 +3332,23 @@ export const dailyPricingInsert = async ( req, res ) => {
3318
3332
  ];
3319
3333
  let dailyData = await dailyPriceService.aggregate( query );
3320
3334
  let cameraDetails = await cameraService.find( { storeId: getStore[storeIndex].storeId, clientId: requestClient[clientIndex], isActivated: true, isUp: true }, { streamName: 1, productModule: 1 } );
3335
+ console.log( '🚀 ~ dailyPricingInsert ~ cameraDetails:', cameraDetails );
3321
3336
 
3322
- let cameraCount = cameraDetails.filter( ( cam ) =>
3337
+ let trafficCameraCount = cameraDetails.filter( ( cam ) =>
3323
3338
  ( cam.productModule || [] ).some( ( mod ) =>
3324
- ( mod.name === 'tangoTraffic' || mod.name === 'tangoTracking' ) && mod.checked === true,
3339
+ ( mod.productName === 'tangoTraffic' || mod.productName === 'tangoTracking' ) && mod.checked === true,
3325
3340
  ),
3326
3341
  ).length;
3342
+
3343
+ let zoneCameraCount = cameraDetails.filter( ( cam ) =>
3344
+ ( cam.productModule || [] ).some( ( mod ) =>
3345
+ mod.productName === 'tangoZone' && mod.checked === true,
3346
+ ),
3347
+ ).length;
3348
+
3327
3349
  let zoneCameraStreamNames = cameraDetails.filter( ( cam ) =>
3328
3350
  ( cam.productModule || [] ).some( ( mod ) =>
3329
- mod.name === 'tangoZone' && mod.checked === true,
3351
+ mod.productName === 'tangoZone' && mod.checked === true,
3330
3352
  ),
3331
3353
  ).map( ( cam ) => cam.streamName );
3332
3354
  let taggingDetails = await taggingService.find( { storeId: getStore[storeIndex].storeId, clientId: requestClient[clientIndex], productName: 'tangoZone', coordinates: { $ne: [] }, streamName: { $in: zoneCameraStreamNames } }, { tagName: 1 } );
@@ -3336,7 +3358,7 @@ export const dailyPricingInsert = async ( req, res ) => {
3336
3358
  let workingdays;
3337
3359
  let workingdaystrax;
3338
3360
  const givenDate = dayjs( requestData.date );
3339
-
3361
+ console.log( '🚀 ~ dailyPricingInsert ~ cameraCount:', trafficCameraCount, zoneCameraCount, zoneCount );
3340
3362
  const isFirstDayOfMonth = givenDate.isSame( dayjs().startOf( 'month' ), 'day' );
3341
3363
  if ( getStore[storeIndex]?.edge.firstFile ) {
3342
3364
  if ( firstDate < requestData.date && getStore[storeIndex]?.status == 'active' &&
@@ -3413,7 +3435,8 @@ export const dailyPricingInsert = async ( req, res ) => {
3413
3435
  daysDifferenceTrax: workingdaystrax,
3414
3436
  products: productList,
3415
3437
  camera: cameraDetails.map( ( item ) => item.streamName ),
3416
- cameraCount: cameraCount,
3438
+ trafficCameraCount: trafficCameraCount,
3439
+ zoneCameraCount: zoneCameraCount,
3417
3440
  zoneCount: zoneCount,
3418
3441
  zoneName: zoneName,
3419
3442
  },
@@ -3843,7 +3866,7 @@ export const invoiceRevised = async ( req, res ) => {
3843
3866
  ...invoiceDetails._doc,
3844
3867
  amount: amount,
3845
3868
  discount: invoiceDetails.discount,
3846
- currencyType: clientDetails?.paymentInvoice?.currencyType == 'dollar' ? '$' : '₹',
3869
+ currencyType: symbolFor( clientDetails?.paymentInvoice?.currencyType ),
3847
3870
  total: amount.toFixed( 2 ),
3848
3871
  invoiceDate,
3849
3872
  dueDate,
@@ -371,6 +371,31 @@ export const updateBillingGroupsSchema = {
371
371
  body: updateBillingGroupBody,
372
372
  };
373
373
 
374
+ export const bulkUpdateBillingGroupRowSchema = joi.object( {
375
+ _id: joi.string().length( 24 ).hex().required(),
376
+ groupName: joi.string().required(),
377
+ groupTag: joi.string().required(),
378
+ registeredCompanyName: joi.string().required(),
379
+ gst: joi.string().allow( '' ).optional(),
380
+ addressLineOne: joi.string().allow( '' ).optional(),
381
+ addressLineTwo: joi.string().allow( '' ).optional(),
382
+ city: joi.string().allow( '' ).optional(),
383
+ state: joi.string().allow( '' ).optional(),
384
+ country: joi.string().allow( '' ).optional(),
385
+ pinCode: joi.string().allow( '' ).optional(),
386
+ placeOfSupply: joi.string().allow( '' ).optional(),
387
+ po: joi.string().allow( '' ).optional(),
388
+ proRata: joi.string().allow( '' ).optional(),
389
+ paymentCategory: joi.string().allow( '' ).optional(),
390
+ currency: joi.string().allow( '' ).optional(),
391
+ paymentCycle: joi.string().allow( '' ).optional(),
392
+ paymentTerm: joi.number().integer().min( 0 ).max( 365 ).optional(),
393
+ installationFee: joi.number().min( 0 ).optional(),
394
+ isInstallationOneTime: joi.boolean().optional(),
395
+ attachAnnexure: joi.boolean().optional(),
396
+ advanceInvoice: joi.boolean().optional(),
397
+ } );
398
+
374
399
  export const deleteBillingGroupQuery = {
375
400
  _id: joi.string().required(),
376
401
  };
@@ -468,3 +493,33 @@ export const clientProductsParam = joi.object().keys( {
468
493
  export const clientProductsValid = {
469
494
  params: clientProductsParam,
470
495
  };
496
+
497
+ const INVOICE_STATUS_VALUES = [ 'pendingCsm', 'pendingFinance', 'pendingApproval', 'approved' ];
498
+
499
+ export const invoiceStatusEnum = INVOICE_STATUS_VALUES;
500
+
501
+ export const approveInvoiceTransitionBody = joi.object( {
502
+ invoiceId: joi.string().length( 24 ).hex().required(),
503
+ } );
504
+
505
+ export const approveInvoiceTransitionSchema = {
506
+ body: approveInvoiceTransitionBody,
507
+ };
508
+
509
+ export const updateInvoiceHeadsBody = joi.object( {
510
+ data: joi.array().items(
511
+ joi.object( {
512
+ role: joi.string().valid( 'csm', 'finance' ).required(),
513
+ // email may be a single string (csm) or an array of strings (finance).
514
+ // Empty string / empty array are allowed for un-setting.
515
+ email: joi.alternatives().try(
516
+ joi.string().email().allow( '' ),
517
+ joi.array().items( joi.string().email() ),
518
+ ).required(),
519
+ } ),
520
+ ).min( 1 ).required(),
521
+ } );
522
+
523
+ export const updateInvoiceHeadsSchema = {
524
+ body: updateInvoiceHeadsBody,
525
+ };
@@ -1612,6 +1612,14 @@
1612
1612
  {{#eq billingCurrency 'dollar'}}
1613
1613
  US Dollar
1614
1614
  {{/eq}}
1615
+
1616
+ {{#eq billingCurrency 'singaporedollar'}}
1617
+ Singapore Dollar
1618
+ {{/eq}}
1619
+
1620
+ {{#eq billingCurrency 'euro'}}
1621
+ Euro
1622
+ {{/eq}}
1615
1623
  {{amountwords}} Only
1616
1624
  </div>
1617
1625
  </div>
@@ -1,7 +1,21 @@
1
1
 
2
2
  import express from 'express';
3
- import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo } from '../controllers/brandsBilling.controller.js';
3
+ import multer from 'multer';
4
+ import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo, bulkDownloadBillingGroups, bulkUpdateBillingGroups } from '../controllers/brandsBilling.controller.js';
4
5
  import { isAllowedSessionHandler, accessVerification } from 'tango-app-api-middleware';
6
+
7
+ const bulkUpload = multer( {
8
+ storage: multer.memoryStorage(),
9
+ limits: { fileSize: 5 * 1024 * 1024 },
10
+ fileFilter: ( _req, file, cb ) => {
11
+ const ok = [
12
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
13
+ 'application/vnd.ms-excel',
14
+ 'application/octet-stream',
15
+ ].includes( file.mimetype );
16
+ cb( ok ? null : new Error( 'Invalid file format — use XLSX' ), ok );
17
+ },
18
+ } );
5
19
  export const brandsBillingRouter = express.Router();
6
20
 
7
21
  brandsBillingRouter.post( '/brandsBillingList', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), brandsBillingList );
@@ -11,3 +25,5 @@ brandsBillingRouter.post( '/brandBillingGroups', isAllowedSessionHandler, access
11
25
  brandsBillingRouter.put( '/updateDailyPricingWorkingDays', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), updateDailyPricingWorkingDays );
12
26
  brandsBillingRouter.put( '/updateDailyPricingStoreField', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), updateDailyPricingStoreField );
13
27
  brandsBillingRouter.post( '/getClientBillingInfo', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getClientBillingInfo );
28
+ brandsBillingRouter.get( '/bulk-download-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), bulkDownloadBillingGroups );
29
+ brandsBillingRouter.post( '/bulk-update-billing-groups', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), bulkUpload.single( 'file' ), bulkUpdateBillingGroups );
@@ -1,11 +1,14 @@
1
1
  import express from 'express';
2
- import { createInvoice, invoiceDownload, clientInvoiceList, creditTransactionlist, pendingInvoices, applyDiscount, migrateInvoice, PaymentStatusChange, checkPaymentStatus, getInvoice, updateInvoice, getClientBasePricing } from '../controllers/invoice.controller.js';
3
- import { isAllowedSessionHandler, accessVerification } from 'tango-app-api-middleware';
2
+ import { createInvoice, invoiceDownload, invoiceDownloadBulk, clientInvoiceList, creditTransactionlist, pendingInvoices, applyDiscount, migrateInvoice, PaymentStatusChange, checkPaymentStatus, getInvoice, invoiceAnnexure, updateInvoice, getClientBasePricing, deleteInvoice, approveInvoiceCsm, approveInvoiceFinance, approveInvoiceApproval } from '../controllers/invoice.controller.js';
3
+ import { isAllowedSessionHandler, accessVerification, validate } from 'tango-app-api-middleware';
4
+ import { getInvoiceHeads, updateInvoiceHeads } from '../controllers/applicationDefault.controllers.js';
5
+ import { updateInvoiceHeadsSchema } from '../dtos/validation.dtos.js';
4
6
  export const invoiceRouter = express.Router();
5
7
 
6
8
 
7
9
  invoiceRouter.post( '/createInvoice', isAllowedSessionHandler, createInvoice );
8
10
  invoiceRouter.post( '/regerateInvoice', isAllowedSessionHandler, createInvoice );
11
+ invoiceRouter.post( '/invoiceDownload/bulk', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), invoiceDownloadBulk );
9
12
  invoiceRouter.post( '/invoiceDownload/:invoiceId', isAllowedSessionHandler, invoiceDownload );
10
13
  invoiceRouter.post( '/clientInvoiceList', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), clientInvoiceList );
11
14
  invoiceRouter.post( '/creditTransactionlist', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), creditTransactionlist );
@@ -15,7 +18,14 @@ invoiceRouter.post( '/migrateInvoice', migrateInvoice );
15
18
  invoiceRouter.post( '/PaymentStatusChange', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), PaymentStatusChange );
16
19
  invoiceRouter.post( '/checkPaymentStatus', checkPaymentStatus );
17
20
  invoiceRouter.get( '/getInvoice/:invoiceId', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), getInvoice );
21
+ invoiceRouter.get( '/invoiceAnnexure/:invoiceId', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), invoiceAnnexure );
18
22
  invoiceRouter.put( '/updateInvoice', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), updateInvoice );
19
23
  invoiceRouter.get( '/getClientBasePricing/:clientId', isAllowedSessionHandler, getClientBasePricing );
24
+ invoiceRouter.delete( '/deleteInvoice/:invoiceId', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), deleteInvoice );
20
25
 
26
+ invoiceRouter.post( '/approveInvoiceCsm', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'csmApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceCsm );
27
+ invoiceRouter.post( '/approveInvoiceFinance', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'financeApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceFinance );
28
+ invoiceRouter.post( '/approveInvoiceApproval', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [ 'isEdit' ] } ] } ), approveInvoiceApproval );
21
29
 
30
+ invoiceRouter.get( '/getInvoiceHeads', isAllowedSessionHandler, getInvoiceHeads );
31
+ invoiceRouter.post( '/updateInvoiceHeads', isAllowedSessionHandler, validate( updateInvoiceHeadsSchema ), updateInvoiceHeads );
@@ -0,0 +1,13 @@
1
+ import model from 'tango-api-schema';
2
+
3
+ export async function findOneApplicationDefault( query, projection ) {
4
+ return model.applicationDefaultModel.findOne( query, projection );
5
+ }
6
+
7
+ export async function upsertApplicationDefault( query, payload ) {
8
+ return model.applicationDefaultModel.findOneAndUpdate(
9
+ query,
10
+ { $set: payload },
11
+ { new: true, upsert: true },
12
+ );
13
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Centralised currency symbol map for invoices, transactions, and PDFs.
3
+ * Keys must match the values stored in billing.currency / transaction.currency
4
+ * enums (see tango-api-schema). Unknown keys fall back to '$' to preserve the
5
+ * pre-existing two-currency-ternary behaviour.
6
+ */
7
+ export const CURRENCY_SYMBOLS = {
8
+ inr: '₹',
9
+ dollar: '$',
10
+ singaporedollar: 'S$',
11
+ euro: '€',
12
+ };
13
+
14
+ export const symbolFor = ( currency ) => CURRENCY_SYMBOLS[currency] ?? '$';