From 4cc97bd224307c4909e024666a50d040926a1b8a Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Fri, 16 Oct 2026 09:52:41 +0530 Subject: [PATCH] Use branch state for walk-in GST and normalize customer states --- api/src/helpers/bill-payload.js | 10 +---- api/src/helpers/gst-place.js | 29 ++++++++++++++ api/src/repositories/sale.repository.js | 8 ++-- api/src/services/sale.service.js | 11 +++++- api/tests/unit/helpers/gst-place.test.js | 23 +++++++++++ api/tests/unit/services/sale.service.test.js | 40 ++++++++++++++++++++ 6 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 api/src/helpers/gst-place.js create mode 100644 api/tests/unit/helpers/gst-place.test.js diff --git a/api/src/helpers/bill-payload.js b/api/src/helpers/bill-payload.js index 00f71438..9da6183d 100644 --- a/api/src/helpers/bill-payload.js +++ b/api/src/helpers/bill-payload.js @@ -254,15 +254,7 @@ function taxRows(sale, branch, allocation = null) { const taxMinor = money.toMinor(tax, policy); const firstHalf = Math.ceil(taxMinor / 2); const rate = gstRate(sale); - const stateKey = (value) => { - const key = String(value || '') - .trim() - .toLowerCase(); - const match = require('../json/gst_state_code.json').gststate.find( - (row) => row.id === key.padStart(2, '0') || row.value.toLowerCase() === key - ); - return match ? match.id : key; - }; + const { stateKey } = require('./gst-place'); const supplierState = stateKey(sale.branch_state || branch.state); const supplyState = stateKey(sale.place_of_supply || sale.customer_state); if (supplierState && supplyState && supplierState !== supplyState) { diff --git a/api/src/helpers/gst-place.js b/api/src/helpers/gst-place.js new file mode 100644 index 00000000..acfa037c --- /dev/null +++ b/api/src/helpers/gst-place.js @@ -0,0 +1,29 @@ +const { gststate } = require('../json/gst_state_code.json'); + +function stateKey(value) { + const key = String(value || '') + .trim() + .toLowerCase() + .replace(/\s+/g, ' '); + const state = gststate.find( + (row) => row.id === key.padStart(2, '0') || row.value.toLowerCase() === key + ); + return state ? state.id : key; +} + +function isInterState(supplierState, customerState) { + const supplier = stateKey(supplierState); + const customer = stateKey(customerState); + return Boolean(supplier && customer && supplier !== customer); +} + +function customerState(data, branch, customer) { + const supplier = String(branch.state || '').trim(); + const isDefault = + !data.customer_id || + (branch.default_customer && String(data.customer_id) === String(branch.default_customer)); + if (isDefault) return supplier; + return String(data.customer_state || customer?.state || '').trim() || supplier; +} + +module.exports = { stateKey, isInterState, customerState }; diff --git a/api/src/repositories/sale.repository.js b/api/src/repositories/sale.repository.js index 60f0de45..67d14b96 100644 --- a/api/src/repositories/sale.repository.js +++ b/api/src/repositories/sale.repository.js @@ -949,10 +949,10 @@ class SalesRepository { const customerState = (doc.customer_state || '').toString().trim(); const branchState = (branchDoc?.state || '').toString().trim(); - const isInterState = - customerState && - branchState && - customerState.toLowerCase() !== branchState.toLowerCase(); + const isInterState = require('../helpers/gst-place').isInterState( + branchState, + customerState + ); if (isInterState) { cloned.igst_tax = perLineGstAmount; diff --git a/api/src/services/sale.service.js b/api/src/services/sale.service.js index 1726d29d..bd7963e0 100644 --- a/api/src/services/sale.service.js +++ b/api/src/services/sale.service.js @@ -426,6 +426,13 @@ const processSale = async ( const pricingCustomer = data.customer_id ? await customerRepository.findById(data.customer_id) : null; + const gstPlace = require('../helpers/gst-place'); + const supplierState = context.branchState || pricingBranch.state || ''; + const saleCustomerState = gstPlace.customerState( + data, + { ...pricingBranch, state: supplierState }, + pricingCustomer + ); let priceList; if (pricingCustomer?.category_id) { const pricingDb = await BaseModel.getDb(); @@ -662,7 +669,7 @@ const processSale = async ( const indianGstSetting = context.branchSettings?.indian_gst || 'gst_off'; - const interPlace = (data.customer_state || '').trim() !== (context.branchState || '').trim(); + const interPlace = gstPlace.isInterState(supplierState, saleCustomerState); if (indianGstSetting === 'gst_on' && gstValue > 0) { if (interPlace) { igst_value = gstValue; @@ -1274,7 +1281,7 @@ const processSale = async ( } : {}), customer_email: (data.customer_email || '').trim(), - customer_state: (data.customer_state || '').trim(), + customer_state: saleCustomerState, customer_country: (data.customer_country || '').trim(), country_sort: finalCountrySort, customer_gst_type: (data.customer_gst_type || '').trim(), diff --git a/api/tests/unit/helpers/gst-place.test.js b/api/tests/unit/helpers/gst-place.test.js new file mode 100644 index 00000000..cddd9d08 --- /dev/null +++ b/api/tests/unit/helpers/gst-place.test.js @@ -0,0 +1,23 @@ +const { stateKey, isInterState, customerState } = require('../../../src/helpers/gst-place'); + +test.each([ + ['Tamil Nadu', '33', false], + ['33', ' tamil nadu ', false], + ['Tamil Nadu', 'Karnataka', true], + ['33', '29', true], + ['33', '', false], + ['', '29', false], +])('GST state comparison %s / %s is interstate: %s', (supplier, customer, expected) => { + expect(isInterState(supplier, customer)).toBe(expected); +}); + +test('walk-in and configured default customers use the branch state', () => { + const branch = { state: 'Tamil Nadu', default_customer: 'walkin-id' }; + expect(customerState({}, branch)).toBe('Tamil Nadu'); + expect(customerState({ customer_id: 'walkin-id', customer_state: '29' }, branch)).toBe( + 'Tamil Nadu' + ); + expect(customerState({ customer_id: 'named-id' }, branch, { state: '29' })).toBe('29'); + expect(customerState({ customer_id: 'named-id' }, branch)).toBe('Tamil Nadu'); + expect(stateKey(' 33 ')).toBe('33'); +}); diff --git a/api/tests/unit/services/sale.service.test.js b/api/tests/unit/services/sale.service.test.js index 26f6aa55..fe88197c 100644 --- a/api/tests/unit/services/sale.service.test.js +++ b/api/tests/unit/services/sale.service.test.js @@ -187,6 +187,46 @@ describe('SalesService', () => { mockRegisterRepositoryInstance.validateSessionOwner.mockResolvedValue({ status: true }); }); + test.each([ + ['', undefined, '', false], + ['64f8f2f4c2b9c0a1e4b22222', '', '', false], + ['64f8f2f4c2b9c0a1e4b22222', '33', '', false], + ['64f8f2f4c2b9c0a1e4b22222', ' tamil nadu ', '', false], + ['64f8f2f4c2b9c0a1e4b22222', 'Karnataka', '', true], + ['64f8f2f4c2b9c0a1e4b22222', '29', '', true], + ['64f8f2f4c2b9c0a1e4b22222', '29', '64f8f2f4c2b9c0a1e4b22222', false], + ])( + 'saves GST by customer state (%s, %s, default %s)', + async (customer_id, customer_state, default_customer, inter) => { + mockItemRepositoryInstance.findItemById.mockResolvedValue( + makeItemDoc({ selling_price: 100, tax: 18, tax_type: 'exclusive' }) + ); + const result = await salesService.processSale( + makeSaleData({ + customer_id, + customer_state, + sales_total: '236', + multi_payment: { Cash: 236 }, + }), + '', + 'Add', + makeContext({ + branchState: 'Tamil Nadu', + branchSettings: { sortname: 'IN', indian_gst: 'gst_on', default_customer }, + }) + ); + expect(result.status).toBe(true); + const saved = salesRepository.create.mock.calls[0][0]; + expect(saved.items[0].igst_tax).toBe(inter ? 36 : 0); + expect(saved.items[0].cgst_tax).toBe(inter ? 0 : 18); + expect(saved.items[0].sgst_tax).toBe(inter ? 0 : 18); + expect(saved.items[0].tax_components.map((component) => component.name)).toEqual( + inter ? ['IGST'] : ['CGST', 'SGST'] + ); + if (!customer_state || default_customer) expect(saved.customer_state).toBe('Tamil Nadu'); + } + ); + afterEach(() => { process.removeListener(KOT_EVENT, onKotReady); consoleErrorSpy.mockRestore();