tango-app-api-payment-subscription 3.5.15 → 3.5.17

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.
@@ -1,6 +1,6 @@
1
1
 
2
2
  /* eslint-disable new-cap */
3
- import { logger, download, sendEmailWithSES, insertOpenSearchData, getOpenSearchData, updateOpenSearchData, getOpenSearchById } from 'tango-app-api-middleware';
3
+ import { logger, download, sendEmailWithSES, insertOpenSearchData, getOpenSearchData, updateOpenSearchData, getOpenSearchById, fileUpload, customSignedUrl } from 'tango-app-api-middleware';
4
4
  import * as paymentService from '../services/clientPayment.services.js';
5
5
  import * as basePriceService from '../services/basePrice.service.js';
6
6
  import * as storeService from '../services/store.service.js';
@@ -85,6 +85,7 @@ export const clientBillingSubscriptionInfo = async ( req, res, next ) => {
85
85
  billingDetails: 1,
86
86
  price: 1,
87
87
  priceType: 1,
88
+ billingGroupWisePricing: 1,
88
89
  virtualAccount: 1,
89
90
  paymentInvoice: 1,
90
91
  },
@@ -259,6 +260,7 @@ export const clientBillingSubscriptionInfo = async ( req, res, next ) => {
259
260
  currentPlanInfo.dueLimitReached = getPI;
260
261
  currentPlanInfo.price = clientInfo[0].price || '--';
261
262
  currentPlanInfo.priceType = clientInfo[0].priceType || '--';
263
+ currentPlanInfo.billingGroupWisePricing = clientInfo[0].billingGroupWisePricing || false;
262
264
  currentPlanInfo.subscriptionType = clientInfo[0].planDetails.subscriptionType || '--';
263
265
  currentPlanInfo.subscriptionPeriod = clientInfo[0].planDetails.subscriptionPeriod || '--';
264
266
  currentPlanInfo.storeCount = storeCount || '--';
@@ -2246,7 +2248,15 @@ export const invoiceList = async ( req, res ) => {
2246
2248
 
2247
2249
  export const priceList = async ( req, res ) => {
2248
2250
  try {
2249
- let pricingDetails = await basePricingService.findOne( { clientId: { $exists: true }, clientId: req.body.clientId }, { standard: 1, step: 1 } );
2251
+ // Group-wise pricing: when a groupId is sent, read that group's own
2252
+ // basepricing doc; otherwise read the brand-level doc (groupId unset).
2253
+ const priceQuery = { clientId: req.body.clientId };
2254
+ if ( req.body.groupId ) {
2255
+ priceQuery.groupId = req.body.groupId;
2256
+ } else {
2257
+ priceQuery.groupId = { $exists: false };
2258
+ }
2259
+ let pricingDetails = await basePricingService.findOne( priceQuery, { standard: 1, step: 1, oneTimeFeePerStore: 1 } );
2250
2260
  if ( !pricingDetails ) {
2251
2261
  return res.sendError( 'no data found', 204 );
2252
2262
  }
@@ -2301,19 +2311,25 @@ export const priceList = async ( req, res ) => {
2301
2311
  product.showImg = true;
2302
2312
  product.showEditDelete = true;
2303
2313
  }
2304
- product.storeCount = item.storeCount;
2314
+ // Last tier absorbs whatever stores remain after the earlier tiers.
2315
+ // Never let it go negative when the actual store count is smaller
2316
+ // than the defined tier ranges (e.g. 83 stores across 1-50 / 51-100
2317
+ // left the last tier at 83 - 100 = -17).
2318
+ product.storeCount = Math.max( item.storeCount, 0 );
2305
2319
  product.lastIndex = true;
2306
- } else if ( index == 0 ) {
2307
- product.storeCount = 100;
2308
- item.storeCount = item.storeCount - 100;
2309
2320
  } else {
2310
- product.showImg = true;
2311
- let rangeArray = product.storeRange.split( '-' );
2321
+ // Non-last tier: use its own range size (end - start + 1), but cap at
2322
+ // the stores still remaining so a tier can't claim more stores than
2323
+ // exist. The first tier previously hardcoded 100, which overcounted
2324
+ // (and pushed the last tier negative) whenever the range wasn't 1-100.
2325
+ product.showImg = index != 0;
2326
+ let rangeArray = String( product.storeRange || '' ).split( '-' );
2312
2327
  let startNumber = parseInt( rangeArray[0] );
2313
2328
  let endNumber = parseInt( rangeArray[1] );
2314
- let diff = endNumber - startNumber + 1;
2315
- product.storeCount = diff;
2316
- item.storeCount = item.storeCount - diff;
2329
+ let diff = ( endNumber - startNumber + 1 ) || 0;
2330
+ let assigned = Math.max( Math.min( diff, item.storeCount ), 0 );
2331
+ product.storeCount = assigned;
2332
+ item.storeCount = item.storeCount - assigned;
2317
2333
  }
2318
2334
  }
2319
2335
  // let discountPrice = product.basePrice * ( product.discountPercentage / 100 );
@@ -2335,6 +2351,7 @@ export const priceList = async ( req, res ) => {
2335
2351
  let finalValue = parseFloat( discountTotalPrice ) + gstAmount;
2336
2352
  let result = {
2337
2353
  product: data,
2354
+ oneTimeFeePerStore: pricingDetails.oneTimeFeePerStore != null ? pricingDetails.oneTimeFeePerStore : null,
2338
2355
  totalActualPrice: originalTotalPrice,
2339
2356
  totalNegotiatePrice: discountTotalPrice.toFixed( 2 ),
2340
2357
  actualPrice: totalProductPrice,
@@ -2353,7 +2370,15 @@ export const priceList = async ( req, res ) => {
2353
2370
 
2354
2371
  export const pricingListUpdate = async ( req, res ) => {
2355
2372
  try {
2356
- let getPriceInfo = await basePricingService.findOne( { clientId: { $exists: true }, clientId: req.body.clientId }, { standard: 1, step: 1 } );
2373
+ // Group-wise pricing: target the group's own basepricing doc when a groupId
2374
+ // is sent; otherwise the brand-level doc (groupId unset).
2375
+ const priceQuery = { clientId: req.body.clientId };
2376
+ if ( req.body.groupId ) {
2377
+ priceQuery.groupId = req.body.groupId;
2378
+ } else {
2379
+ priceQuery.groupId = { $exists: false };
2380
+ }
2381
+ let getPriceInfo = await basePricingService.findOne( priceQuery, { standard: 1, step: 1 } );
2357
2382
  let findClient = await paymentService.findOneClient( { clientId: req.body.clientId } );
2358
2383
 
2359
2384
  console.log( getPriceInfo );
@@ -2363,8 +2388,10 @@ export const pricingListUpdate = async ( req, res ) => {
2363
2388
  pricingType: findClient.priceType,
2364
2389
  };
2365
2390
 
2366
- if ( findClient.priceType==='standard' ) {
2367
- getPriceInfo.standard.map( ( item ) => {
2391
+ // Old-data snapshot for the audit log. Guard on getPriceInfo: a brand-new
2392
+ // billing-group doc has none yet, and the no-doc path is handled below.
2393
+ if ( getPriceInfo && findClient.priceType==='standard' ) {
2394
+ ( getPriceInfo.standard || [] ).map( ( item ) => {
2368
2395
  oldData = {
2369
2396
  ...oldData,
2370
2397
  [item.productName+' '+'negotiatePrice']: item.negotiatePrice,
@@ -2376,8 +2403,8 @@ export const pricingListUpdate = async ( req, res ) => {
2376
2403
  };
2377
2404
  }
2378
2405
  } );
2379
- } else {
2380
- getPriceInfo.step.map( ( item ) => {
2406
+ } else if ( getPriceInfo ) {
2407
+ ( getPriceInfo.step || [] ).map( ( item ) => {
2381
2408
  oldData = {
2382
2409
  ...oldData,
2383
2410
  [item.productName+' '+'negotiatePrice']: item.negotiatePrice,
@@ -2484,6 +2511,18 @@ export const pricingListUpdate = async ( req, res ) => {
2484
2511
  } else {
2485
2512
  getPriceInfo.step = req.body.products;
2486
2513
  }
2514
+ // Brand-level one-time fee per store. Only overwrite when the request
2515
+ // actually carries a value so other save paths don't wipe it.
2516
+ if ( req.body.oneTimeFeePerStore != null && req.body.oneTimeFeePerStore !== '' ) {
2517
+ getPriceInfo.oneTimeFeePerStore = Number( req.body.oneTimeFeePerStore ) || 0;
2518
+ }
2519
+ // Keep the group identity on the doc when saving group-wise pricing.
2520
+ if ( req.body.groupId ) {
2521
+ getPriceInfo.groupId = req.body.groupId;
2522
+ if ( req.body.groupName ) {
2523
+ getPriceInfo.groupName = req.body.groupName;
2524
+ }
2525
+ }
2487
2526
  getPriceInfo.save().then( async () => {
2488
2527
  let clientDetails = await paymentService.findOne( { clientId: req.body.clientId }, { priceType: 1, paymentInvoice: 1, planDetails: 1 } );
2489
2528
  clientDetails.priceType = req.body.type;
@@ -2554,7 +2593,15 @@ export const pricingListUpdate = async ( req, res ) => {
2554
2593
 
2555
2594
  async function updatePricing( req, res, update ) {
2556
2595
  let baseProduct = await basePricingService.findOne( { clientId: { $exists: false } }, { basePricing: 1 } );
2557
- let getPriceInfo = await basePricingService.findOne( { clientId: { $exists: true }, clientId: req.body.clientId }, { standard: 1, step: 1 } );
2596
+ // Group-wise pricing: scope the doc to the group when a groupId is sent;
2597
+ // otherwise the brand-level doc (groupId unset).
2598
+ const pricingDocQuery = { clientId: req.body.clientId };
2599
+ if ( req.body.groupId ) {
2600
+ pricingDocQuery.groupId = req.body.groupId;
2601
+ } else {
2602
+ pricingDocQuery.groupId = { $exists: false };
2603
+ }
2604
+ let getPriceInfo = await basePricingService.findOne( pricingDocQuery, { standard: 1, step: 1 } );
2558
2605
  let clientDetails = await paymentService.findOne( { clientId: req.body.clientId } );
2559
2606
  if ( clientDetails ) {
2560
2607
  let products = clientDetails.planDetails.product.map( ( item ) => item.productName );
@@ -2625,12 +2672,20 @@ async function updatePricing( req, res, update ) {
2625
2672
  step: stepList,
2626
2673
  clientId: req.body.clientId,
2627
2674
  };
2675
+ // Stamp the group identity so group-wise pricing docs are distinguishable
2676
+ // from the brand-level doc.
2677
+ if ( req.body.groupId ) {
2678
+ data.groupId = req.body.groupId;
2679
+ if ( req.body.groupName ) {
2680
+ data.groupName = req.body.groupName;
2681
+ }
2682
+ }
2628
2683
  console.log( '🚀 ~ updatePricing ~ data:', data );
2629
2684
  if ( !getPriceInfo ) {
2630
2685
  await basePricingService.create( data );
2631
2686
  } else {
2632
2687
  delete data.clientId;
2633
- await basePricingService.updateOne( { clientId: req.body.clientId }, data );
2688
+ await basePricingService.updateOne( pricingDocQuery, data );
2634
2689
  }
2635
2690
  let product = [];
2636
2691
  let clientId = req.body.clientId;
@@ -4130,3 +4185,113 @@ export async function createDefaultbillings( req, res ) {
4130
4185
  }
4131
4186
  }
4132
4187
 
4188
+ // ---------------------------------------------------------------------------
4189
+ // Brand documents (Plans & Subscription > Documents accordion).
4190
+ // Upload a single PDF for a client, store it in the assets bucket under
4191
+ // <clientId>/documents/, and record { documentName, path } on the client's
4192
+ // additionalDocuments array. List returns the docs with signed URLs.
4193
+ // ---------------------------------------------------------------------------
4194
+ export async function uploadClientDocument( req, res ) {
4195
+ try {
4196
+ const clientId = String( req.body?.clientId || '' );
4197
+ const documentName = String( req.body?.documentName || '' ).trim();
4198
+ const expiryDateRaw = req.body?.expiryDate;
4199
+ const file = req.file; // multer single('file')
4200
+
4201
+ if ( !clientId ) {
4202
+ return res.sendError( 'clientId is required', 400 );
4203
+ }
4204
+ if ( !documentName ) {
4205
+ return res.sendError( 'documentName is required', 400 );
4206
+ }
4207
+ if ( !file ) {
4208
+ return res.sendError( 'file is required', 400 );
4209
+ }
4210
+ // PDF only.
4211
+ if ( file.mimetype !== 'application/pdf' ) {
4212
+ return res.sendError( 'Only PDF files are allowed', 400 );
4213
+ }
4214
+
4215
+ const client = await paymentService.findOneClient( { clientId }, { clientId: 1 } );
4216
+ if ( !client ) {
4217
+ return res.sendError( 'Client not found', 404 );
4218
+ }
4219
+
4220
+ const bucket = JSON.parse( process.env.BUCKET );
4221
+ // Unique-ish file name so re-uploads with the same display name don't clash.
4222
+ const safeName = documentName.replace( /[^a-zA-Z0-9-_]/g, '_' );
4223
+ const fileName = `${safeName}_${Date.now()}.pdf`;
4224
+ const key = `${clientId}/documents/`;
4225
+
4226
+ await fileUpload( {
4227
+ Bucket: bucket.assets,
4228
+ Key: key,
4229
+ fileName: fileName,
4230
+ ContentType: file.mimetype,
4231
+ body: file.buffer,
4232
+ } );
4233
+
4234
+ const storedPath = `${key}${fileName}`;
4235
+ const expiryDate = expiryDateRaw ? new Date( expiryDateRaw ) : null;
4236
+ await paymentService.pushAdditionalDocument( { clientId }, {
4237
+ documentName: documentName,
4238
+ path: storedPath,
4239
+ expiryDate: ( expiryDate && !isNaN( expiryDate.getTime() ) ) ? expiryDate : null,
4240
+ uploadedAt: new Date(),
4241
+ } );
4242
+
4243
+ return res.sendSuccess( { documentName, path: storedPath, expiryDate } );
4244
+ } catch ( error ) {
4245
+ logger.error( { error: error, function: 'uploadClientDocument' } );
4246
+ return res.sendError( error, 500 );
4247
+ }
4248
+ }
4249
+
4250
+ export async function getClientDocuments( req, res ) {
4251
+ try {
4252
+ const clientId = String( req.query?.clientId || req.params?.clientId || '' );
4253
+ if ( !clientId ) {
4254
+ return res.sendError( 'clientId is required', 400 );
4255
+ }
4256
+ const client = await paymentService.findOneClient( { clientId }, { additionalDocuments: 1 } );
4257
+ const bucket = JSON.parse( process.env.BUCKET );
4258
+ const docs = await Promise.all( ( client?.additionalDocuments || [] ).map( async ( d ) => {
4259
+ let url = '';
4260
+ try {
4261
+ url = await customSignedUrl( { Bucket: bucket.assets, file_path: d.path }, 8 );
4262
+ } catch ( e ) {
4263
+ url = '';
4264
+ }
4265
+ return {
4266
+ _id: d._id,
4267
+ documentName: d.documentName,
4268
+ path: d.path,
4269
+ expiryDate: d.expiryDate,
4270
+ uploadedAt: d.uploadedAt,
4271
+ url,
4272
+ };
4273
+ } ) );
4274
+ return res.sendSuccess( { documents: docs } );
4275
+ } catch ( error ) {
4276
+ logger.error( { error: error, function: 'getClientDocuments' } );
4277
+ return res.sendError( error, 500 );
4278
+ }
4279
+ }
4280
+
4281
+ // Toggle billing-group-wise pricing on the client. When enabled, pricing is
4282
+ // maintained per billing group (separate basepricing docs keyed by groupId).
4283
+ export async function setBillingGroupWisePricing( req, res ) {
4284
+ try {
4285
+ const clientId = String( req.body?.clientId || '' );
4286
+ const enabled = req.body?.enabled === true || req.body?.enabled === 'true';
4287
+ if ( !clientId ) {
4288
+ return res.sendError( 'clientId is required', 400 );
4289
+ }
4290
+ await paymentService.updateOne( { clientId }, { billingGroupWisePricing: enabled } );
4291
+ return res.sendSuccess( { clientId, billingGroupWisePricing: enabled } );
4292
+ } catch ( error ) {
4293
+ logger.error( { error: error, function: 'setBillingGroupWisePricing' } );
4294
+ return res.sendError( error, 500 );
4295
+ }
4296
+ }
4297
+
@@ -153,6 +153,11 @@ export const validatePriceSchema = joi.object( {
153
153
  type: joi.string().optional(),
154
154
  clientId: joi.string().required(),
155
155
  products: joi.array().optional(),
156
+ oneTimeFeePerStore: joi.number().optional().allow( null, '' ),
157
+ // Billing-group-wise pricing: when present, the pricing is saved to / read
158
+ // from the group's own basepricing doc instead of the brand-level one.
159
+ groupId: joi.string().optional().allow( null, '' ),
160
+ groupName: joi.string().optional().allow( null, '' ),
156
161
  pricing: joi.array().items( joi.object( {
157
162
  productName: joi.string().required(),
158
163
  negotiatePrice: joi.number().required(),
@@ -177,6 +182,8 @@ export const revisedParams = {
177
182
  export const validatePriceListSchema = joi.object( {
178
183
  priceType: joi.string().required(),
179
184
  clientId: joi.string().required(),
185
+ // Optional billing group filter for group-wise pricing.
186
+ groupId: joi.string().optional().allow( null, '' ),
180
187
  } );
181
188
 
182
189
  export const validatePriceListParams = {
@@ -1,6 +1,6 @@
1
1
 
2
2
  import express from 'express';
3
- import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo, bulkDownloadBillingGroups, bulkUpdateBillingGroups, billingSummary, additionalProducts } from '../controllers/brandsBilling.controller.js';
3
+ import { brandsBillingList, brandInvoiceList, latestDailyPricing, brandBillingGroups, updateDailyPricingWorkingDays, updateDailyPricingStoreField, getClientBillingInfo, bulkDownloadBillingGroups, bulkUpdateBillingGroups, billingSummary, additionalProducts, additionalProductExport } from '../controllers/brandsBilling.controller.js';
4
4
  import { isAllowedSessionHandler, accessVerification } from 'tango-app-api-middleware';
5
5
 
6
6
  export const brandsBillingRouter = express.Router();
@@ -17,3 +17,4 @@ brandsBillingRouter.post( '/bulk-update-billing-groups', isAllowedSessionHandler
17
17
  brandsBillingRouter.post( '/billingSummary', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), billingSummary );
18
18
  brandsBillingRouter.get( '/billingSummary', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), billingSummary );
19
19
  brandsBillingRouter.get( '/additionalProducts', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), additionalProducts );
20
+ brandsBillingRouter.get( '/additionalProductExport', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'TangoAdmin', name: 'invoiceApproval', permissions: [] } ] } ), additionalProductExport );
@@ -1,11 +1,16 @@
1
1
 
2
2
  import express from 'express';
3
+ import multer from 'multer';
3
4
  import * as paymentController from '../controllers/paymentSubscription.controllers.js';
4
5
  import { validate, isAllowedSessionHandler, accessVerification } from 'tango-app-api-middleware';
5
6
  import * as validationDtos from '../dtos/validation.dtos.js';
6
7
  import { validateClient } from '../utils/validations/client.validation.js';
7
8
  export const paymentSubscriptionRouter = express.Router();
8
9
 
10
+ // PDF document upload (Plans & Subscription > Documents). In-memory storage,
11
+ // 10MB cap; the controller streams the buffer to the assets S3 bucket.
12
+ const documentUpload = multer( { storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } } );
13
+
9
14
  paymentSubscriptionRouter.post( '/addBilling', isAllowedSessionHandler, accessVerification( {
10
15
  userType: [ 'tango', 'client' ], access: [
11
16
  { featureName: 'Global', name: 'Subscription', permissions: [ 'isAdd' ] },
@@ -146,4 +151,11 @@ paymentSubscriptionRouter.put( '/pushNotification/update/:notificationId', isAll
146
151
  paymentSubscriptionRouter.post( '/updateRemind/:notificationId', isAllowedSessionHandler, validate( validationDtos.validateId ), paymentController.updateRemind );
147
152
  paymentSubscriptionRouter.post( '/createDefaultbillings', paymentController.createDefaultbillings );
148
153
 
154
+ // Brand documents (Plans & Subscription > Documents accordion).
155
+ paymentSubscriptionRouter.post( '/client-document/upload', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Subscription', permissions: [ 'isEdit' ] } ] } ), documentUpload.single( 'file' ), paymentController.uploadClientDocument );
156
+ paymentSubscriptionRouter.get( '/client-document/list', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Subscription', permissions: [] } ] } ), paymentController.getClientDocuments );
157
+
158
+ // Toggle billing-group-wise pricing for a client (tango only, edit perm).
159
+ paymentSubscriptionRouter.post( '/billingGroupWisePricing', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ], access: [ { featureName: 'Global', name: 'Subscription', permissions: [ 'isEdit' ] } ] } ), paymentController.setBillingGroupWisePricing );
160
+
149
161
 
@@ -16,6 +16,11 @@ export const updateOne = ( query = {}, record = {} ) => {
16
16
  return model.clientModel.updateOne( query, { $set: record } );
17
17
  };
18
18
 
19
+ // Push a document into the client's additionalDocuments array.
20
+ export const pushAdditionalDocument = ( query = {}, document = {} ) => {
21
+ return model.clientModel.updateOne( query, { $push: { additionalDocuments: document } } );
22
+ };
23
+
19
24
  export const aggregate = ( query = [] ) => {
20
25
  return model.clientModel.aggregate( query );
21
26
  };