From 31ba64c4ea7400547c16a801eaa24f705b757308 Mon Sep 17 00:00:00 2001 From: Pratap2018 Date: Wed, 7 Oct 2026 13:30:13 +0530 Subject: [PATCH 1/4] add credit calculator --- package.json | 4 + scripts/cache-kyc-catalog.js | 57 ++ src/catalogs/catalog.kyc.json | 558 +++++++++++++ src/components/credit/KycCreditCalculator.vue | 772 ++++++++++++++++++ src/store/creditCalculatorStore.js | 37 + src/store/index.js | 5 +- src/views/playground/KYCDashboardCredit.vue | 162 +++- tests/unit/KycCreditCalculator.spec.js | 141 ++++ tests/unit/creditCalculatorStore.spec.js | 21 + 9 files changed, 1728 insertions(+), 29 deletions(-) create mode 100644 scripts/cache-kyc-catalog.js create mode 100644 src/catalogs/catalog.kyc.json create mode 100644 src/components/credit/KycCreditCalculator.vue create mode 100644 src/store/creditCalculatorStore.js create mode 100644 tests/unit/KycCreditCalculator.spec.js create mode 100644 tests/unit/creditCalculatorStore.spec.js diff --git a/package.json b/package.json index 604ef83f..29fd1fc1 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,12 @@ "scripts": { "serve": "vue-cli-service serve --mode production", "dev": "vue-cli-service serve --mode staging", + "catalog:cache": "node scripts/cache-kyc-catalog.js", + "catalog:refresh": "node scripts/cache-kyc-catalog.js --refresh", + "prebuild": "npm run catalog:cache", "setEnv": "cp .env.staging .env", "build": "vue-cli-service build", + "prebuild-stage": "npm run catalog:cache", "build-stage": "vue-cli-service build --mode staging", "lint": "vue-cli-service lint", "test:unit": "vue-cli-service test:unit", diff --git a/scripts/cache-kyc-catalog.js b/scripts/cache-kyc-catalog.js new file mode 100644 index 00000000..090f2c5d --- /dev/null +++ b/scripts/cache-kyc-catalog.js @@ -0,0 +1,57 @@ +const fs = require('fs'); +const https = require('https'); +const path = require('path'); + +const catalogUrl = 'https://raw.githubusercontent.com/hypersign-protocol/hypersign-credits-middleware/refs/heads/ADR000/src/catalogs/catalog.kyc.json'; +const catalogPath = path.resolve(__dirname, '../src/catalogs/catalog.kyc.json'); +const shouldRefresh = process.argv.includes('--refresh'); + +function hasValidCache() { + if (!fs.existsSync(catalogPath)) return false; + + try { + const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8')); + return Array.isArray(catalog.routes) && catalog.routes.length > 0; + } catch (_) { + return false; + } +} + +if (!shouldRefresh && hasValidCache()) { + console.log(`Using cached KYC credit catalog at ${catalogPath}`); + process.exit(0); +} + +fs.mkdirSync(path.dirname(catalogPath), { recursive: true }); + +https.get(catalogUrl, response => { + if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { + console.error('The KYC catalog URL redirected. Update the pinned catalog URL before building.'); + process.exit(1); + } + + if (response.statusCode !== 200) { + console.error(`Unable to download KYC catalog: HTTP ${response.statusCode}`); + process.exit(1); + } + + let body = ''; + response.setEncoding('utf8'); + response.on('data', chunk => { body += chunk; }); + response.on('end', () => { + try { + const catalog = JSON.parse(body); + if (!Array.isArray(catalog.routes) || catalog.routes.length === 0) { + throw new Error('catalog does not contain routes'); + } + fs.writeFileSync(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`); + console.log(`Cached KYC credit catalog at ${catalogPath}`); + } catch (error) { + console.error(`Unable to cache KYC catalog: ${error.message}`); + process.exit(1); + } + }); +}).on('error', error => { + console.error(`Unable to download KYC catalog: ${error.message}`); + process.exit(1); +}); diff --git a/src/catalogs/catalog.kyc.json b/src/catalogs/catalog.kyc.json new file mode 100644 index 00000000..e3414409 --- /dev/null +++ b/src/catalogs/catalog.kyc.json @@ -0,0 +1,558 @@ +{ + "serviceType": "CAVACH_API", + "version": "3.23.0", + "globalPrefix": "api", + "versioning": "URI", + "uriVersionPrefix": "v", + "defaultVersion": "1", + "routes": [ + { + "method": "POST", + "path": "/api/v1/aadhaar/face/match", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 8 + } + ] + }, + { + "method": "POST", + "path": "/api/v1/aadhaar/otp/generate", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 2 + } + ] + }, + { + "method": "POST", + "path": "/api/v1/aadhaar/otp/verify", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 5 + } + ] + }, + { + "method": "POST", + "path": "/api/v1/aadhaar/qr/verify", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 7 + } + ] + }, + { + "method": "GET", + "path": "/api/v1/compliance", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/compliance", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 100 + } + ] + }, + { + "method": "GET", + "path": "/api/v1/credit", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/credit", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/credit/:creditId", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/credit/:creditId/activate", + "charges": [] + }, + { + "method": "DELETE", + "path": "/api/v1/document/:id", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/document/:id/verification", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 20 + } + ] + }, + { + "method": "GET", + "path": "/api/v1/document/download/:id", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/document/upload", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 10 + } + ] + }, + { + "method": "GET", + "path": "/api/v1/e-kyb/verification/company", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyb/verification/company", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 100 + } + ] + }, + { + "method": "GET", + "path": "/api/v1/e-kyb/verification/company/:companyId/company-executives", + "charges": [] + }, + { + "method": "PATCH", + "path": "/api/v1/e-kyb/verification/company/:companyId/company-executives", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyb/verification/company/:companyId/company-executives", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 1 + } + ] + }, + { + "method": "DELETE", + "path": "/api/v1/e-kyb/verification/company/:companyId/company-executives/:id", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyb/verification/company/:companyId/company-executives/:id", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyb/verification/company/:companyId/company-executives/resend-mail", + "charges": [] + }, + { + "method": "DELETE", + "path": "/api/v1/e-kyb/verification/company/:id", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyb/verification/company/:id", + "charges": [] + }, + { + "method": "PATCH", + "path": "/api/v1/e-kyb/verification/company/:id", + "charges": [] + }, + { + "method": "PATCH", + "path": "/api/v1/e-kyb/verification/company/:id/status", + "charges": [] + }, + { + "method": "DELETE", + "path": "/api/v1/e-kyb/verification/widget-config", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyb/verification/widget-config", + "charges": [] + }, + { + "method": "PATCH", + "path": "/api/v1/e-kyb/verification/widget-config", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyb/verification/widget-config", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/analytics/demographic-stats", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/analytics/device-stats", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/analytics/overview", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/asset-metadata", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/auth", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 1 + } + ] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/doc-ocr", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 30 + } + ] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/doc-ocr/extract", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 30 + } + ] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/doc-ocr/status/:sessionId", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/mobile-flow", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/mobile-flow/:flowId", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 10 + } + ] + }, + { + "method": "PATCH", + "path": "/api/v1/e-kyc/verification/mobile-flow/:flowId", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/mobile-flow/:flowId/status", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/onchainkyc-config", + "charges": [] + }, + { + "method": "PATCH", + "path": "/api/v1/e-kyc/verification/onchainkyc-config", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/onchainkyc-config", + "charges": [] + }, + { + "method": "DELETE", + "path": "/api/v1/e-kyc/verification/onchainkyc-config/:onchainConfigId", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/onchainkyc-config/:onchainConfigId", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/passive-liveliness", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 15 + } + ] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/passive-liveliness/status/:sessionId", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/sbt-mint", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/session", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/session", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 4 + } + ] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/session/:sessionId", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/sessions/:sessionId/decision", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 4 + } + ] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/user-consent", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/user-consent", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 5 + } + ] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/webhook-config", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/webhook-config", + "charges": [] + }, + { + "method": "DELETE", + "path": "/api/v1/e-kyc/verification/webhook-config/:id", + "charges": [] + }, + { + "method": "PATCH", + "path": "/api/v1/e-kyc/verification/webhook-config/:id", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/widget-config", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/widget-config", + "charges": [] + }, + { + "method": "DELETE", + "path": "/api/v1/e-kyc/verification/widget-config/:widgetConfigId", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/widget-config/:widgetConfigId", + "charges": [] + }, + { + "method": "PATCH", + "path": "/api/v1/e-kyc/verification/widget-config/:widgetConfigId", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/e-kyc/verification/widget-config/all", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/e-kyc/verification/zk-proof", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 10 + } + ] + }, + { + "method": "POST", + "path": "/api/v1/mail-client/add-job", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/proof", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/usage", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/usage/detail", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/usage/invoice", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/user", + "charges": [] + }, + { + "method": "GET", + "path": "/api/v1/user/:userId", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v1/wallet", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v2/auth/exchange", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 1 + } + ] + }, + { + "method": "POST", + "path": "/api/v2/biometrics/verify", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 45 + } + ] + }, + { + "method": "GET", + "path": "/api/v2/claims/consent", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v2/consents", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 5 + } + ] + }, + { + "method": "GET", + "path": "/api/v2/consents/:sessionId", + "charges": [] + }, + { + "method": "POST", + "path": "/api/v2/documents/extract", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 20 + } + ] + }, + { + "method": "POST", + "path": "/api/v2/session", + "charges": [ + { + "id": "api", + "creditType": "API_CREDIT", + "amount": 4 + } + ] + } + ] +} diff --git a/src/components/credit/KycCreditCalculator.vue b/src/components/credit/KycCreditCalculator.vue new file mode 100644 index 00000000..298fb8ca --- /dev/null +++ b/src/components/credit/KycCreditCalculator.vue @@ -0,0 +1,772 @@ + + + + + + + diff --git a/src/store/creditCalculatorStore.js b/src/store/creditCalculatorStore.js new file mode 100644 index 00000000..1c7a515d --- /dev/null +++ b/src/store/creditCalculatorStore.js @@ -0,0 +1,37 @@ +const emptyConfiguration = () => ({ + selectedFlowIds: [], + selectedServiceIds: [], + creditsPerVerification: 0, + updatedAt: null +}) + +const uniqueStrings = (values) => Array.from(new Set( + (Array.isArray(values) ? values : []).filter(value => typeof value === 'string') +)) + +const creditCalculatorStore = { + namespaced: true, + state: { + configuration: emptyConfiguration() + }, + getters: { + getConfiguration: (state) => state.configuration + }, + mutations: { + saveConfiguration(state, payload = {}) { + state.configuration = { + selectedFlowIds: uniqueStrings(payload.selectedFlowIds), + selectedServiceIds: uniqueStrings(payload.selectedServiceIds), + creditsPerVerification: Number.isFinite(Number(payload.creditsPerVerification)) + ? Math.max(Number(payload.creditsPerVerification), 0) + : 0, + updatedAt: new Date().toISOString() + } + }, + clearConfiguration(state) { + state.configuration = emptyConfiguration() + } + } +} + +export default creditCalculatorStore diff --git a/src/store/index.js b/src/store/index.js index b3133679..a649a45a 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -4,6 +4,7 @@ import playgroundStore from './playgroundStore'; import mainStore from './mainStore'; import globalStore from './globalStore'; import walletStore from './walletStore' +import creditCalculatorStore from './creditCalculatorStore' import createPersistedState from 'vuex-persistedstate'; Vue.use(Vuex) @@ -12,8 +13,8 @@ export default new Vuex.Store({ playgroundStore, mainStore, globalStore, - walletStore + walletStore, + creditCalculatorStore }, plugins: [createPersistedState()] }) - diff --git a/src/views/playground/KYCDashboardCredit.vue b/src/views/playground/KYCDashboardCredit.vue index 7fb2841a..1a11b39f 100644 --- a/src/views/playground/KYCDashboardCredit.vue +++ b/src/views/playground/KYCDashboardCredit.vue @@ -50,7 +50,28 @@ / {{ numberFormat(myKYCCredits.allAvailableCredits) }} Credits -

upto ~{{ numberFormat(approxKYCLeftBestCase) }} ID Verifications

+
@@ -156,6 +177,11 @@ + @@ -165,10 +191,11 @@ import { mapActions, mapGetters } from "vuex"; import UtilsMixin from '../../mixins/utils'; import HfButtons from "../../components/element/HfButtons.vue"; import AccessDenied from '../AccessDenied.vue'; +import KycCreditCalculator from '../../components/credit/KycCreditCalculator.vue'; export default { - name: "SSIDashboardCredit", - components: { HfButtons, AccessDenied }, + name: "KYCDashboardCredit", + components: { HfButtons, AccessDenied, KycCreditCalculator }, mixins: [UtilsMixin], data() { return { @@ -180,7 +207,7 @@ export default { accessDeniedMsg: '', fullPage: true, doughNutChartLabel: ['Used', 'Remaining'], - analyticsOverview: null, + calculatorOpen: false, } }, computed: { @@ -197,23 +224,21 @@ export default { return 'color-green'; }, - // Formula: (1/completionRate) accounts for sessions that don't complete - // + dropOffRate adds waste from abandoned sessions - // Example: 70% completion, 24% drop-off → (1/0.7) + 0.24 = 1.67 → ~125 credits/ID - costPerKYC() { - if (!this.analyticsOverview) return 75; // no data yet: assume best case - const completionRate = Math.max((this.analyticsOverview.completionRate || 100), 1) / 100; - const dropOff = (this.analyticsOverview.dropOffRate || 0) / 100; - const multiplier = Math.min(Math.max((1 / completionRate) + dropOff, 1.0), 2.5); - return Math.ceil(75 * multiplier); + savedCalculatorConfiguration() { + return this.$store.getters['creditCalculatorStore/getConfiguration'] || {}; }, - approxKYCLeft() { - return Math.floor((this.myKYCCredits.allRemainingCredits || 0) / this.costPerKYC); + hasSavedCalculatorConfiguration() { + const configuration = this.savedCalculatorConfiguration; + return Array.isArray(configuration.selectedFlowIds) + && configuration.selectedFlowIds.length > 0 + && Number(configuration.creditsPerVerification) > 0; }, - approxKYCLeftBestCase() { - return Math.floor((this.myKYCCredits.allRemainingCredits || 0) / 75); + savedVerificationEstimate() { + const cost = Number(this.savedCalculatorConfiguration.creditsPerVerification || 0); + if (!cost) return 0; + return Math.floor(Math.max(this.myKYCCredits.allRemainingCredits, 0) / cost); }, myKYCCredits() { @@ -253,18 +278,13 @@ export default { this.stopTimer(); }, methods: { - ...mapActions('mainStore', ['fetchKYCCredits', 'activateCredit', 'fetchAnalyticsOverview']), + ...mapActions('mainStore', ['fetchKYCCredits', 'activateCredit']), async reloadData() { try { this.isLoading = true; await this.fetchKYCCredits(); - // Non-blocking: fetch analytics to refine estimate - this.fetchAnalyticsOverview({ env: this.$store.getters['mainStore/getSelectedService']?.env || 'prod' }) - .then(res => { if (res?.data) this.analyticsOverview = res.data; }) - .catch(() => {}); - this.startTimer(); this.renderChart(); } catch (e) { @@ -458,9 +478,74 @@ export default { /* Keeps label close to the value below it */ } -.approx-inline { - font-size: 0.75rem; - color: #9ca3af; +.calculator-callout { + display: grid; + width: 100%; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 0.85rem; + align-items: center; + padding: 0.75rem 0.9rem; + border: 1px solid #dbeafe; + border-radius: 0.65rem; + background: #eff6ff; + color: #1e3a5f; + text-align: left; + transition: border-color .15s ease, background-color .15s ease; +} + +.calculator-callout:hover { + border-color: #93c5fd; + background: #eaf3ff; +} + +.calculator-callout:focus-visible { + outline: 3px solid rgba(59, 130, 246, .28); + outline-offset: 2px; +} + +.calculator-callout-icon { + display: inline-flex; + width: 2.5rem; + height: 2.5rem; + align-items: center; + justify-content: center; + border-radius: 50%; + background: #dbeafe; +} + +.calculator-callout-icon .v-icon, +.calculator-callout-action, +.calculator-callout-action .v-icon { + color: #1677ff; +} + +.calculator-callout-copy { + display: flex; + min-width: 0; + flex-direction: column; +} + +.calculator-callout-copy strong { + color: #172554; + font-size: 0.86rem; +} + +.calculator-callout-copy small { + margin-top: 0.1rem; + color: #64748b; + font-size: 0.76rem; +} + +.calculator-callout-action { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.7rem 0.85rem; + border-radius: 0.55rem; + background: #dceaff; + font-size: 0.78rem; + font-weight: 700; + white-space: nowrap; } /* Ensure the button doesn't have extra margin pushing it away from the edge */ @@ -490,4 +575,27 @@ export default { vertical-align: middle; } - \ No newline at end of file +@media (max-width: 767.98px) { + .py-3 { + width: 100% !important; + } + + .calculator-callout { + grid-template-columns: auto minmax(0, 1fr); + } + + .calculator-callout-action { + grid-column: 2; + justify-self: start; + } + + .usage-table-wrapper { + overflow-x: auto; + } + + .usage-table { + min-width: 760px; + } +} + + diff --git a/tests/unit/KycCreditCalculator.spec.js b/tests/unit/KycCreditCalculator.spec.js new file mode 100644 index 00000000..e1c6c2cf --- /dev/null +++ b/tests/unit/KycCreditCalculator.spec.js @@ -0,0 +1,141 @@ +import { expect } from 'chai' +import { shallowMount } from '@vue/test-utils' +import KycCreditCalculator from '@/components/credit/KycCreditCalculator.vue' + +const mountCalculator = (options = {}) => shallowMount(KycCreditCalculator, { + propsData: { + remainingCredits: 8779, + totalCredits: 10100 + }, + ...options +}) + +describe('KycCreditCalculator.vue', () => { + it('selects the India defaults and keeps Aadhaar Verification required', () => { + const wrapper = mountCalculator() + + wrapper.vm.selectFlow('india') + + expect(wrapper.vm.selectedFlowIds).to.deep.equal(['india']) + expect(wrapper.vm.selectedServiceIds).to.include.members([ + 'aadhaar-face-match', + 'aadhaar-verification' + ]) + expect(wrapper.vm.flows[0].services.find(service => service.id === 'aadhaar-verification').required).to.equal(true) + }) + + it('selects the Global defaults but leaves Age Verification opt-in', () => { + const wrapper = mountCalculator() + + wrapper.vm.selectFlow('global') + + expect(wrapper.vm.selectedServiceIds).to.include.members([ + 'verification-session', + 'passive-liveness', + 'document-verification', + 'consent-capture' + ]) + expect(wrapper.vm.selectedServiceIds).not.to.include('age-verification') + }) + + it('selects all Business services and adds the required Global defaults for UBO', () => { + const wrapper = mountCalculator() + + wrapper.vm.selectFlow('business') + + expect(wrapper.vm.selectedFlowIds).to.include.members(['business', 'global']) + expect(wrapper.vm.selectedServiceIds).to.include.members([ + 'business-data-capture', + 'ubo-verification', + 'aml-screening', + 'verification-session', + 'passive-liveness', + 'document-verification', + 'consent-capture' + ]) + expect(wrapper.vm.isFlowLocked('global')).to.equal(true) + expect(wrapper.vm.creditsPerVerification).to.equal(245) + expect(wrapper.vm.estimatedVerifications).to.equal(35) + }) + + it('toggles optional services without changing required services', () => { + const wrapper = mountCalculator() + wrapper.vm.selectFlow('global') + + const documentVerification = wrapper.vm.flows[1].services.find(service => service.id === 'document-verification') + const verificationSession = wrapper.vm.flows[1].services.find(service => service.id === 'verification-session') + + wrapper.vm.toggleService(documentVerification) + expect(wrapper.vm.selectedServiceIds).not.to.include('document-verification') + + wrapper.vm.toggleService(documentVerification) + expect(wrapper.vm.selectedServiceIds).to.include('document-verification') + + wrapper.vm.toggleService(verificationSession) + expect(wrapper.vm.selectedServiceIds).to.include('verification-session') + }) + + it('keeps every Business service selected and disabled', () => { + const wrapper = mountCalculator() + wrapper.vm.selectFlow('business') + const businessServices = wrapper.vm.flows[2].services + + businessServices.forEach(service => wrapper.vm.toggleService(service)) + + expect(businessServices.every(service => service.required)).to.equal(true) + expect(wrapper.vm.selectedServiceIds).to.include.members(businessServices.map(service => service.id)) + expect(wrapper.vm.isFlowLocked('global')).to.equal(true) + }) + + it('saves the selected configuration to the calculator store', () => { + const commits = [] + const wrapper = mountCalculator({ + mocks: { + $store: { + getters: { 'creditCalculatorStore/getConfiguration': null }, + commit: (type, payload) => commits.push({ type, payload }) + } + } + }) + wrapper.vm.selectFlow('india') + + wrapper.vm.saveConfiguration() + + expect(commits).to.have.length(1) + expect(commits[0].type).to.equal('creditCalculatorStore/saveConfiguration') + expect(commits[0].payload.selectedFlowIds).to.deep.equal(['india']) + expect(commits[0].payload.selectedServiceIds).to.include.members([ + 'aadhaar-face-match', + 'aadhaar-verification' + ]) + expect(commits[0].payload.creditsPerVerification).to.equal(wrapper.vm.creditsPerVerification) + }) + + it('restores saved selections and repairs required dependencies', () => { + const wrapper = mountCalculator({ + mocks: { + $store: { + getters: { + 'creditCalculatorStore/getConfiguration': { + selectedFlowIds: ['business'], + selectedServiceIds: [] + } + }, + commit: () => {} + } + } + }) + + wrapper.vm.loadSavedConfiguration() + + expect(wrapper.vm.selectedFlowIds).to.deep.equal(['global', 'business']) + expect(wrapper.vm.selectedServiceIds).to.include.members([ + 'verification-session', + 'passive-liveness', + 'consent-capture', + 'business-data-capture', + 'ubo-verification', + 'aml-screening' + ]) + }) +}) diff --git a/tests/unit/creditCalculatorStore.spec.js b/tests/unit/creditCalculatorStore.spec.js new file mode 100644 index 00000000..e005b986 --- /dev/null +++ b/tests/unit/creditCalculatorStore.spec.js @@ -0,0 +1,21 @@ +import { expect } from 'chai' +import creditCalculatorStore from '@/store/creditCalculatorStore' + +describe('creditCalculatorStore', () => { + it('stores isolated copies of the calculator selection', () => { + const state = { configuration: {} } + const payload = { + selectedFlowIds: ['india', 'india'], + selectedServiceIds: ['aadhaar-verification'], + creditsPerVerification: 12 + } + + creditCalculatorStore.mutations.saveConfiguration(state, payload) + payload.selectedFlowIds.push('global') + + expect(state.configuration.selectedFlowIds).to.deep.equal(['india']) + expect(state.configuration.selectedServiceIds).to.deep.equal(['aadhaar-verification']) + expect(state.configuration.creditsPerVerification).to.equal(12) + expect(state.configuration.updatedAt).to.be.a('string') + }) +}) From b3111538b572690fee329ae2801967f1bca1e646 Mon Sep 17 00:00:00 2001 From: Pratap2018 Date: Thu, 8 Oct 2026 12:35:17 +0530 Subject: [PATCH 2/4] update --- src/components/credit/KycCreditCalculator.vue | 555 ++++++------------ .../credit/VerificationFlowConfigurator.vue | 403 +++++++++++++ src/store/creditCalculatorStore.js | 14 +- src/views/playground/KYCDashboardCredit.vue | 22 +- tests/unit/KycCreditCalculator.spec.js | 147 +++-- .../unit/VerificationFlowConfigurator.spec.js | 113 ++++ tests/unit/creditCalculatorStore.spec.js | 2 + 7 files changed, 843 insertions(+), 413 deletions(-) create mode 100644 src/components/credit/VerificationFlowConfigurator.vue create mode 100644 tests/unit/VerificationFlowConfigurator.spec.js diff --git a/src/components/credit/KycCreditCalculator.vue b/src/components/credit/KycCreditCalculator.vue index 298fb8ca..6dc8b419 100644 --- a/src/components/credit/KycCreditCalculator.vue +++ b/src/components/credit/KycCreditCalculator.vue @@ -17,83 +17,15 @@
-
-
-

1. What do you want to verify?

-

Choose one or more verification flows to see their services.

- -
- -
-
- -
-

2. Select services

-

{{ selectedFlows.length ? 'Review the services included in each verification.' : 'Select one or more verification flows first.' }}

- -
-
-
- {{ flow.icon }} - {{ flow.title }} -
-
-
- - - - - {{ service.icon }} - - - {{ service.title }} - Required - - {{ service.description }} - - mdi-information-outline{{ service.note }} - - - {{ serviceCost(service) }} {{ serviceCost(service) === 1 ? 'credit' : 'credits' }} -
-
-
-
- -
- mdi-cursor-default-click-outline - Choose one or more flows above to configure their services. -
-
-
+
- +
+
+ + mdi-format-list-checks + +
+ Configuration summary + {{ configurationSummary.title }} + {{ configurationSummary.detail }} +
+
+ + +
@@ -130,6 +75,7 @@ + + diff --git a/src/store/creditCalculatorStore.js b/src/store/creditCalculatorStore.js index 1c7a515d..14390f23 100644 --- a/src/store/creditCalculatorStore.js +++ b/src/store/creditCalculatorStore.js @@ -1,6 +1,7 @@ const emptyConfiguration = () => ({ selectedFlowIds: [], selectedServiceIds: [], + serviceQuantities: {}, creditsPerVerification: 0, updatedAt: null }) @@ -19,9 +20,20 @@ const creditCalculatorStore = { }, mutations: { saveConfiguration(state, payload = {}) { + const selectedServiceIds = uniqueStrings(payload.selectedServiceIds) + const rawQuantities = payload.serviceQuantities && typeof payload.serviceQuantities === 'object' + ? payload.serviceQuantities + : {} + const serviceQuantities = Object.keys(rawQuantities).reduce((quantities, serviceId) => { + if (!selectedServiceIds.includes(serviceId)) return quantities + quantities[serviceId] = Math.max(Math.floor(Number(rawQuantities[serviceId]) || 1), 1) + return quantities + }, {}) + state.configuration = { selectedFlowIds: uniqueStrings(payload.selectedFlowIds), - selectedServiceIds: uniqueStrings(payload.selectedServiceIds), + selectedServiceIds, + serviceQuantities, creditsPerVerification: Number.isFinite(Number(payload.creditsPerVerification)) ? Math.max(Number(payload.creditsPerVerification), 0) : 0, diff --git a/src/views/playground/KYCDashboardCredit.vue b/src/views/playground/KYCDashboardCredit.vue index 1a11b39f..12e16f6e 100644 --- a/src/views/playground/KYCDashboardCredit.vue +++ b/src/views/playground/KYCDashboardCredit.vue @@ -56,7 +56,7 @@