elpaypg 0.0.0-stage → 2.0.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.
- package/.env.example +8 -0
- package/README.md +176 -2
- package/package.json +21 -4
- package/public/elpay-checkout.js +769 -0
- package/public/elpay-sms-gateway.apk +0 -0
- package/public/elpay-sms-gateway.apk.sha256 +1 -0
- package/public/elpay.js +785 -0
- package/public/fonts/dm-sans.woff2 +0 -0
- package/public/fonts/sora.woff2 +0 -0
- package/public/views/checkout.html +2839 -0
- package/public/views/partials/auth.html +110 -0
- package/public/views/partials/footer.html +54 -0
- package/public/views/partials/header.html +101 -0
- package/public/views/partials/modals.html +179 -0
- package/public/views/partials/payment-methods.html +526 -0
- package/public/views/partials/sidebar.html +75 -0
- package/public/views/partials/splash.html +35 -0
- package/src/__tests__/smoke.test.js +8 -0
- package/src/auth-guard.js +95 -0
- package/src/index.js +137 -0
- package/src/router.js +46 -0
- package/src/routes/admin.routes.js +2421 -0
- package/src/routes/checkout.routes.js +2000 -0
- package/src/routes/customer.routes.js +650 -0
- package/src/routes/device.routes.js +303 -0
- package/src/routes/payments.routes.js +446 -0
- package/src/routes/pollers.routes.js +653 -0
- package/src/services/observer-poller.service.js +369 -0
- package/src/services/tunnel-manager.js +130 -0
- package/src/services/whatsapp-berry.service.js +743 -0
- package/src/vendor/common/__tests__/common.test.js +297 -0
- package/src/vendor/common/__tests__/common.test.ts +160 -0
- package/src/vendor/common/contracts.js +64 -0
- package/src/vendor/common/contracts.ts +188 -0
- package/src/vendor/common/crypto.js +132 -0
- package/src/vendor/common/crypto.ts +112 -0
- package/src/vendor/common/enums.js +91 -0
- package/src/vendor/common/enums.ts +91 -0
- package/src/vendor/common/index.js +12 -0
- package/src/vendor/common/index.ts +8 -0
- package/src/vendor/common/money.js +62 -0
- package/src/vendor/common/money.ts +56 -0
- package/src/vendor/common/observation.js +27 -0
- package/src/vendor/common/observation.ts +52 -0
- package/src/vendor/common/runtime-config.js +246 -0
- package/src/vendor/common/secrets.js +374 -0
- package/src/vendor/common/state-machine.d.ts +1 -0
- package/src/vendor/common/state-machine.js +1 -0
- package/src/vendor/common/state-transitions.js +85 -0
- package/src/vendor/common/tunnel-command.js +59 -0
- package/src/vendor/common/upi-providers.js +389 -0
- package/src/vendor/common/upi-providers.ts +400 -0
- package/src/vendor/common/upi.js +69 -0
- package/src/vendor/common/upi.ts +61 -0
- package/src/vendor/core/__tests__/core.test.js +683 -0
- package/src/vendor/core/__tests__/queue-timing.test.js +237 -0
- package/src/vendor/core/__tests__/webhook-engine.test.js +243 -0
- package/src/vendor/core/attribution-engine.js +855 -0
- package/src/vendor/core/bank-email-parser.js +105 -0
- package/src/vendor/core/bank-sms-parser.js +183 -0
- package/src/vendor/core/device-security.js +506 -0
- package/src/vendor/core/index.js +13 -0
- package/src/vendor/core/normalizer.js +165 -0
- package/src/vendor/core/qr-lib.js +8 -0
- package/src/vendor/core/qr.js +62 -0
- package/src/vendor/core/queue-engine.js +153 -0
- package/src/vendor/core/recovery-engine.js +159 -0
- package/src/vendor/core/state-machine.js +42 -0
- package/src/vendor/core/timing-engine.js +272 -0
- package/src/vendor/core/verification-pipeline.js +1164 -0
- package/src/vendor/core/webhook-engine.js +280 -0
- package/src/vendor/db/__tests__/auth-matrix.test.js +564 -0
- package/src/vendor/db/__tests__/customer-reset.test.js +95 -0
- package/src/vendor/db/__tests__/db.test.js +265 -0
- package/src/vendor/db/__tests__/schema-parity.test.js +137 -0
- package/src/vendor/db/__tests__/sqlite_verify.test.js +15 -0
- package/src/vendor/db/database.js +181 -0
- package/src/vendor/db/index.js +12 -0
- package/src/vendor/db/migrate.js +210 -0
- package/src/vendor/db/repositories/audit-log.repository.js +68 -0
- package/src/vendor/db/repositories/customer-session.repository.js +181 -0
- package/src/vendor/db/repositories/customer.repository.js +636 -0
- package/src/vendor/db/repositories/device.repository.js +285 -0
- package/src/vendor/db/repositories/merchant.repository.js +158 -0
- package/src/vendor/db/repositories/observation.repository.js +124 -0
- package/src/vendor/db/repositories/payment-session.repository.js +618 -0
- package/src/vendor/db/repositories/receiving-account.repository.js +244 -0
- package/src/vendor/db/repositories/support-ticket.repository.js +140 -0
- package/src/vendor/db/schema.sql +266 -0
- package/src/vendor/db/sqlite-schema.sql +270 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentic ISO/IEC 18004 Standard QR Code Engine (Pure JavaScript).
|
|
3
|
+
* Supports Versions 1 through 40, Error Correction Levels L/M/Q/H.
|
|
4
|
+
* 100% genuine scannable QR codes for Android Camera, ZXing, OpenCV, and UPI apps.
|
|
5
|
+
*/
|
|
6
|
+
var qrcode=function(){function i(t,r){function a(t,r){g=function(t){for(var r=new Array(t),e=0;e<t;e+=1){r[e]=new Array(t);for(var n=0;n<t;n+=1)r[e][n]=null}return r}(l=4*u+17),e(0,0),e(l-7,0),e(0,l-7),i(),o(),v(t,r),7<=u&&h(t),null==n&&(n=w(u,f,c)),d(n,r)}var u=t,f=y[r],g=null,l=0,n=null,c=[],s={},e=function(t,r){for(var e=-1;e<=7;e+=1)if(!(t+e<=-1||l<=t+e))for(var n=-1;n<=7;n+=1)r+n<=-1||l<=r+n||(g[t+e][r+n]=0<=e&&e<=6&&(0==n||6==n)||0<=n&&n<=6&&(0==e||6==e)||2<=e&&e<=4&&2<=n&&n<=4)},o=function(){for(var t=8;t<l-8;t+=1)null==g[t][6]&&(g[t][6]=t%2==0);for(var r=8;r<l-8;r+=1)null==g[6][r]&&(g[6][r]=r%2==0)},i=function(){for(var t=B.getPatternPosition(u),r=0;r<t.length;r+=1)for(var e=0;e<t.length;e+=1){var n=t[r],o=t[e];if(null==g[n][o])for(var i=-2;i<=2;i+=1)for(var a=-2;a<=2;a+=1)g[n+i][o+a]=-2==i||2==i||-2==a||2==a||0==i&&0==a}},h=function(t){for(var r=B.getBCHTypeNumber(u),e=0;e<18;e+=1){var n=!t&&1==(r>>e&1);g[Math.floor(e/3)][e%3+l-8-3]=n}for(e=0;e<18;e+=1){n=!t&&1==(r>>e&1);g[e%3+l-8-3][Math.floor(e/3)]=n}},v=function(t,r){for(var e=f<<3|r,n=B.getBCHTypeInfo(e),o=0;o<15;o+=1){var i=!t&&1==(n>>o&1);o<6?g[o][8]=i:o<8?g[o+1][8]=i:g[l-15+o][8]=i}for(o=0;o<15;o+=1){i=!t&&1==(n>>o&1);o<8?g[8][l-o-1]=i:o<9?g[8][15-o-1+1]=i:g[8][15-o-1]=i}g[l-8][8]=!t},d=function(t,r){for(var e=-1,n=l-1,o=7,i=0,a=B.getMaskFunction(r),u=l-1;0<u;u-=2)for(6==u&&(u-=1);;){for(var f=0;f<2;f+=1)if(null==g[n][u-f]){var c=!1;i<t.length&&(c=1==(t[i]>>>o&1)),a(n,u-f)&&(c=!c),g[n][u-f]=c,-1==(o-=1)&&(i+=1,o=7)}if((n+=e)<0||l<=n){n-=e,e=-e;break}}},w=function(t,r,e){for(var n=b.getRSBlocks(t,r),o=M(),i=0;i<e.length;i+=1){var a=e[i];o.put(a.getMode(),4),o.put(a.getLength(),B.getLengthInBits(a.getMode(),t)),a.write(o)}var u=0;for(i=0;i<n.length;i+=1)u+=n[i].dataCount;if(o.getLengthInBits()>8*u)throw"code length overflow. ("+o.getLengthInBits()+">"+8*u+")";for(o.getLengthInBits()+4<=8*u&&o.put(0,4);o.getLengthInBits()%8!=0;)o.putBit(!1);for(;!(o.getLengthInBits()>=8*u||(o.put(236,8),o.getLengthInBits()>=8*u));)o.put(17,8);return function(t,r){for(var e=0,n=0,o=0,i=new Array(r.length),a=new Array(r.length),u=0;u<r.length;u+=1){var f=r[u].dataCount,c=r[u].totalCount-f;n=Math.max(n,f),o=Math.max(o,c),i[u]=new Array(f);for(var g=0;g<i[u].length;g+=1)i[u][g]=255&t.getBuffer()[g+e];e+=f;var l=B.getErrorCorrectPolynomial(c),h=C(i[u],l.getLength()-1).mod(l);a[u]=new Array(l.getLength()-1);for(g=0;g<a[u].length;g+=1){var s=g+h.getLength()-a[u].length;a[u][g]=0<=s?h.getAt(s):0}}var v=0;for(g=0;g<r.length;g+=1)v+=r[g].totalCount;var d=new Array(v),w=0;for(g=0;g<n;g+=1)for(u=0;u<r.length;u+=1)g<i[u].length&&(d[w]=i[u][g],w+=1);for(g=0;g<o;g+=1)for(u=0;u<r.length;u+=1)g<a[u].length&&(d[w]=a[u][g],w+=1);return d}(o,n)};s.addData=function(t,r){var e=null;switch(r=r||"Byte"){case"Numeric":e=x(t);break;case"Alphanumeric":e=m(t);break;case"Byte":e=L(t);break;case"Kanji":e=D(t);break;default:throw"mode:"+r}c.push(e),n=null},s.isDark=function(t,r){if(t<0||l<=t||r<0||l<=r)throw t+","+r;return g[t][r]},s.getModuleCount=function(){return l},s.make=function(){if(u<1){for(var t=1;t<40;t++){for(var r=b.getRSBlocks(t,f),e=M(),n=0;n<c.length;n++){var o=c[n];e.put(o.getMode(),4),e.put(o.getLength(),B.getLengthInBits(o.getMode(),t)),o.write(e)}var i=0;for(n=0;n<r.length;n++)i+=r[n].dataCount;if(e.getLengthInBits()<=8*i)break}u=t}a(!1,function(){for(var t=0,r=0,e=0;e<8;e+=1){a(!0,e);var n=B.getLostPoint(s);(0==e||n<t)&&(t=n,r=e)}return r}())},s.createTableTag=function(t,r){t=t||2;var e="";e+='<table style="',e+=" border-width: 0px; border-style: none;",e+=" border-collapse: collapse;",e+=" padding: 0px; margin: "+(r=void 0===r?4*t:r)+"px;",e+='">',e+="<tbody>";for(var n=0;n<s.getModuleCount();n+=1){e+="<tr>";for(var o=0;o<s.getModuleCount();o+=1)e+='<td style="',e+=" border-width: 0px; border-style: none;",e+=" border-collapse: collapse;",e+=" padding: 0px; margin: 0px;",e+=" width: "+t+"px;",e+=" height: "+t+"px;",e+=" background-color: ",e+=s.isDark(n,o)?"#000000":"#ffffff",e+=";",e+='"/>';e+="</tr>"}return e+="</tbody>",e+="</table>"},s.createSvgTag=function(t,r,e,n){var o={};"object"==typeof t&&(t=(o=t).cellSize,r=o.margin,e=o.alt,n=o.title),t=t||2,r=void 0===r?4*t:r,(e="string"==typeof e?{text:e}:e||{}).text=e.text||null,e.id=e.text?e.id||"qrcode-description":null,(n="string"==typeof n?{text:n}:n||{}).text=n.text||null,n.id=n.text?n.id||"qrcode-title":null;var i,a,u,f,c=s.getModuleCount()*t+2*r,g="";for(f="l"+t+",0 0,"+t+" -"+t+",0 0,-"+t+"z ",g+='<svg version="1.1" xmlns="http://www.w3.org/2000/svg"',g+=o.scalable?"":' width="'+c+'px" height="'+c+'px"',g+=' viewBox="0 0 '+c+" "+c+'" ',g+=' preserveAspectRatio="xMinYMin meet"',g+=n.text||e.text?' role="img" aria-labelledby="'+p([n.id,e.id].join(" ").trim())+'"':"",g+=">",g+=n.text?'<title id="'+p(n.id)+'">'+p(n.text)+"</title>":"",g+=e.text?'<description id="'+p(e.id)+'">'+p(e.text)+"</description>":"",g+='<rect width="100%" height="100%" fill="white" cx="0" cy="0"/>',g+='<path d="',a=0;a<s.getModuleCount();a+=1)for(u=a*t+r,i=0;i<s.getModuleCount();i+=1)s.isDark(a,i)&&(g+="M"+(i*t+r)+","+u+f);return g+='" stroke="transparent" fill="black"/>',g+="</svg>"},s.createDataURL=function(o,t){o=o||2,t=void 0===t?4*o:t;var r=s.getModuleCount()*o+2*t,i=t,a=r-t;return I(r,r,function(t,r){if(i<=t&&t<a&&i<=r&&r<a){var e=Math.floor((t-i)/o),n=Math.floor((r-i)/o);return s.isDark(n,e)?0:1}return 1})},s.createImgTag=function(t,r,e){t=t||2,r=void 0===r?4*t:r;var n=s.getModuleCount()*t+2*r,o="";return o+="<img",o+=' src="',o+=s.createDataURL(t,r),o+='"',o+=' width="',o+=n,o+='"',o+=' height="',o+=n,o+='"',e&&(o+=' alt="',o+=p(e),o+='"'),o+="/>"};var p=function(t){for(var r="",e=0;e<t.length;e+=1){var n=t.charAt(e);switch(n){case"<":r+="<";break;case">":r+=">";break;case"&":r+="&";break;case'"':r+=""";break;default:r+=n}}return r};return s.createASCII=function(t,r){if((t=t||1)<2)return function(t){t=void 0===t?2:t;var r,e,n,o,i,a=1*s.getModuleCount()+2*t,u=t,f=a-t,c={"██":"█","█ ":"▀"," █":"▄"," ":" "},g={"██":"▀","█ ":"▀"," █":" "," ":" "},l="";for(r=0;r<a;r+=2){for(n=Math.floor((r-u)/1),o=Math.floor((r+1-u)/1),e=0;e<a;e+=1)i="█",u<=e&&e<f&&u<=r&&r<f&&s.isDark(n,Math.floor((e-u)/1))&&(i=" "),u<=e&&e<f&&u<=r+1&&r+1<f&&s.isDark(o,Math.floor((e-u)/1))?i+=" ":i+="█",l+=t<1&&f<=r+1?g[i]:c[i];l+="\n"}return a%2&&0<t?l.substring(0,l.length-a-1)+Array(1+a).join("▀"):l.substring(0,l.length-1)}(r);t-=1,r=void 0===r?2*t:r;var e,n,o,i,a=s.getModuleCount()*t+2*r,u=r,f=a-r,c=Array(t+1).join("██"),g=Array(t+1).join(" "),l="",h="";for(e=0;e<a;e+=1){for(o=Math.floor((e-u)/t),h="",n=0;n<a;n+=1)i=1,u<=n&&n<f&&u<=e&&e<f&&s.isDark(o,Math.floor((n-u)/t))&&(i=0),h+=i?c:g;for(o=0;o<t;o+=1)l+=h+"\n"}return l.substring(0,l.length-1)},s.renderTo2dContext=function(t,r){r=r||2;for(var e=s.getModuleCount(),n=0;n<e;n++)for(var o=0;o<e;o++)t.fillStyle=s.isDark(n,o)?"black":"white",t.fillRect(n*r,o*r,r,r)},s}i.stringToBytes=(i.stringToBytesFuncs={default:function(t){for(var r=[],e=0;e<t.length;e+=1){var n=t.charCodeAt(e);r.push(255&n)}return r}}).default,i.createStringToBytes=function(u,f){var i=function(){function t(){var t=r.read();if(-1==t)throw"eof";return t}for(var r=S(u),e=0,n={};;){var o=r.read();if(-1==o)break;var i=t(),a=t()<<8|t();n[String.fromCharCode(o<<8|i)]=a,e+=1}if(e!=f)throw e+" != "+f;return n}(),a="?".charCodeAt(0);return function(t){for(var r=[],e=0;e<t.length;e+=1){var n=t.charCodeAt(e);if(n<128)r.push(n);else{var o=i[t.charAt(e)];"number"==typeof o?(255&o)==o?r.push(o):(r.push(o>>>8),r.push(255&o)):r.push(a)}}return r}};var r,t,a=1,u=2,o=4,f=8,y={L:1,M:0,Q:3,H:2},e=0,n=1,c=2,g=3,l=4,h=5,s=6,v=7,B=(r=[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],(t={}).getBCHTypeInfo=function(t){for(var r=t<<10;0<=d(r)-d(1335);)r^=1335<<d(r)-d(1335);return 21522^(t<<10|r)},t.getBCHTypeNumber=function(t){for(var r=t<<12;0<=d(r)-d(7973);)r^=7973<<d(r)-d(7973);return t<<12|r},t.getPatternPosition=function(t){return r[t-1]},t.getMaskFunction=function(t){switch(t){case e:return function(t,r){return(t+r)%2==0};case n:return function(t,r){return t%2==0};case c:return function(t,r){return r%3==0};case g:return function(t,r){return(t+r)%3==0};case l:return function(t,r){return(Math.floor(t/2)+Math.floor(r/3))%2==0};case h:return function(t,r){return t*r%2+t*r%3==0};case s:return function(t,r){return(t*r%2+t*r%3)%2==0};case v:return function(t,r){return(t*r%3+(t+r)%2)%2==0};default:throw"bad maskPattern:"+t}},t.getErrorCorrectPolynomial=function(t){for(var r=C([1],0),e=0;e<t;e+=1)r=r.multiply(C([1,w.gexp(e)],0));return r},t.getLengthInBits=function(t,r){if(1<=r&&r<10)switch(t){case a:return 10;case u:return 9;case o:case f:return 8;default:throw"mode:"+t}else if(r<27)switch(t){case a:return 12;case u:return 11;case o:return 16;case f:return 10;default:throw"mode:"+t}else{if(!(r<41))throw"type:"+r;switch(t){case a:return 14;case u:return 13;case o:return 16;case f:return 12;default:throw"mode:"+t}}},t.getLostPoint=function(t){for(var r=t.getModuleCount(),e=0,n=0;n<r;n+=1)for(var o=0;o<r;o+=1){for(var i=0,a=t.isDark(n,o),u=-1;u<=1;u+=1)if(!(n+u<0||r<=n+u))for(var f=-1;f<=1;f+=1)o+f<0||r<=o+f||0==u&&0==f||a==t.isDark(n+u,o+f)&&(i+=1);5<i&&(e+=3+i-5)}for(n=0;n<r-1;n+=1)for(o=0;o<r-1;o+=1){var c=0;t.isDark(n,o)&&(c+=1),t.isDark(n+1,o)&&(c+=1),t.isDark(n,o+1)&&(c+=1),t.isDark(n+1,o+1)&&(c+=1),0!=c&&4!=c||(e+=3)}for(n=0;n<r;n+=1)for(o=0;o<r-6;o+=1)t.isDark(n,o)&&!t.isDark(n,o+1)&&t.isDark(n,o+2)&&t.isDark(n,o+3)&&t.isDark(n,o+4)&&!t.isDark(n,o+5)&&t.isDark(n,o+6)&&(e+=40);for(o=0;o<r;o+=1)for(n=0;n<r-6;n+=1)t.isDark(n,o)&&!t.isDark(n+1,o)&&t.isDark(n+2,o)&&t.isDark(n+3,o)&&t.isDark(n+4,o)&&!t.isDark(n+5,o)&&t.isDark(n+6,o)&&(e+=40);var g=0;for(o=0;o<r;o+=1)for(n=0;n<r;n+=1)t.isDark(n,o)&&(g+=1);return e+=Math.abs(100*g/r/r-50)/5*10},t);function d(t){for(var r=0;0!=t;)r+=1,t>>>=1;return r}var w=function(){for(var r=new Array(256),e=new Array(256),t=0;t<8;t+=1)r[t]=1<<t;for(t=8;t<256;t+=1)r[t]=r[t-4]^r[t-5]^r[t-6]^r[t-8];for(t=0;t<255;t+=1)e[r[t]]=t;var n={glog:function(t){if(t<1)throw"glog("+t+")";return e[t]},gexp:function(t){for(;t<0;)t+=255;for(;256<=t;)t-=255;return r[t]}};return n}();function C(n,o){if(void 0===n.length)throw n.length+"/"+o;var r=function(){for(var t=0;t<n.length&&0==n[t];)t+=1;for(var r=new Array(n.length-t+o),e=0;e<n.length-t;e+=1)r[e]=n[e+t];return r}(),i={getAt:function(t){return r[t]},getLength:function(){return r.length},multiply:function(t){for(var r=new Array(i.getLength()+t.getLength()-1),e=0;e<i.getLength();e+=1)for(var n=0;n<t.getLength();n+=1)r[e+n]^=w.gexp(w.glog(i.getAt(e))+w.glog(t.getAt(n)));return C(r,0)},mod:function(t){if(i.getLength()-t.getLength()<0)return i;for(var r=w.glog(i.getAt(0))-w.glog(t.getAt(0)),e=new Array(i.getLength()),n=0;n<i.getLength();n+=1)e[n]=i.getAt(n);for(n=0;n<t.getLength();n+=1)e[n]^=w.gexp(w.glog(t.getAt(n))+r);return C(e,0).mod(t)}};return i}function p(){var e=[],o={writeByte:function(t){e.push(255&t)},writeShort:function(t){o.writeByte(t),o.writeByte(t>>>8)},writeBytes:function(t,r,e){r=r||0,e=e||t.length;for(var n=0;n<e;n+=1)o.writeByte(t[n+r])},writeString:function(t){for(var r=0;r<t.length;r+=1)o.writeByte(t.charCodeAt(r))},toByteArray:function(){return e},toString:function(){var t="";t+="[";for(var r=0;r<e.length;r+=1)0<r&&(t+=","),t+=e[r];return t+="]"}};return o}var k,A,b=(k=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12,7,37,13],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],(A={}).getRSBlocks=function(t,r){var e=function(t,r){switch(r){case y.L:return k[4*(t-1)+0];case y.M:return k[4*(t-1)+1];case y.Q:return k[4*(t-1)+2];case y.H:return k[4*(t-1)+3];default:return}}(t,r);if(void 0===e)throw"bad rs block @ typeNumber:"+t+"/errorCorrectionLevel:"+r;for(var n,o,i=e.length/3,a=[],u=0;u<i;u+=1)for(var f=e[3*u+0],c=e[3*u+1],g=e[3*u+2],l=0;l<f;l+=1)a.push((n=g,o=void 0,(o={}).totalCount=c,o.dataCount=n,o));return a},A),M=function(){var e=[],n=0,o={getBuffer:function(){return e},getAt:function(t){var r=Math.floor(t/8);return 1==(e[r]>>>7-t%8&1)},put:function(t,r){for(var e=0;e<r;e+=1)o.putBit(1==(t>>>r-e-1&1))},getLengthInBits:function(){return n},putBit:function(t){var r=Math.floor(n/8);e.length<=r&&e.push(0),t&&(e[r]|=128>>>n%8),n+=1}};return o},x=function(t){var r=a,n=t,e={getMode:function(){return r},getLength:function(t){return n.length},write:function(t){for(var r=n,e=0;e+2<r.length;)t.put(o(r.substring(e,e+3)),10),e+=3;e<r.length&&(r.length-e==1?t.put(o(r.substring(e,e+1)),4):r.length-e==2&&t.put(o(r.substring(e,e+2)),7))}},o=function(t){for(var r=0,e=0;e<t.length;e+=1)r=10*r+i(t.charAt(e));return r},i=function(t){if("0"<=t&&t<="9")return t.charCodeAt(0)-"0".charCodeAt(0);throw"illegal char :"+t};return e},m=function(t){var r=u,n=t,e={getMode:function(){return r},getLength:function(t){return n.length},write:function(t){for(var r=n,e=0;e+1<r.length;)t.put(45*o(r.charAt(e))+o(r.charAt(e+1)),11),e+=2;e<r.length&&t.put(o(r.charAt(e)),6)}},o=function(t){if("0"<=t&&t<="9")return t.charCodeAt(0)-"0".charCodeAt(0);if("A"<=t&&t<="Z")return t.charCodeAt(0)-"A".charCodeAt(0)+10;switch(t){case" ":return 36;case"$":return 37;case"%":return 38;case"*":return 39;case"+":return 40;case"-":return 41;case".":return 42;case"/":return 43;case":":return 44;default:throw"illegal char :"+t}};return e},L=function(t){var r=o,e=i.stringToBytes(t),n={getMode:function(){return r},getLength:function(t){return e.length},write:function(t){for(var r=0;r<e.length;r+=1)t.put(e[r],8)}};return n},D=function(t){var r=f,e=i.stringToBytesFuncs.SJIS;if(!e)throw"sjis not supported.";!function(){var t=e("友");if(2!=t.length||38726!=(t[0]<<8|t[1]))throw"sjis not supported."}();var o=e(t),n={getMode:function(){return r},getLength:function(t){return~~(o.length/2)},write:function(t){for(var r=o,e=0;e+1<r.length;){var n=(255&r[e])<<8|255&r[e+1];if(33088<=n&&n<=40956)n-=33088;else{if(!(57408<=n&&n<=60351))throw"illegal char at "+(e+1)+"/"+n;n-=49472}n=192*(n>>>8&255)+(255&n),t.put(n,13),e+=2}if(e<r.length)throw"illegal char at "+(e+1)}};return n},S=function(t){var e=t,n=0,o=0,i=0,r={read:function(){for(;i<8;){if(n>=e.length){if(0==i)return-1;throw"unexpected end of file./"+i}var t=e.charAt(n);if(n+=1,"="==t)return i=0,-1;t.match(/^\s$/)||(o=o<<6|a(t.charCodeAt(0)),i+=6)}var r=o>>>i-8&255;return i-=8,r}},a=function(t){if(65<=t&&t<=90)return t-65;if(97<=t&&t<=122)return t-97+26;if(48<=t&&t<=57)return t-48+52;if(43==t)return 62;if(47==t)return 63;throw"c:"+t};return r},I=function(t,r,e){for(var n=function(t,r){var n=t,o=r,l=new Array(t*r),e={setPixel:function(t,r,e){l[r*n+t]=e},write:function(t){t.writeString("GIF87a"),t.writeShort(n),t.writeShort(o),t.writeByte(128),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(255),t.writeByte(255),t.writeByte(255),t.writeString(","),t.writeShort(0),t.writeShort(0),t.writeShort(n),t.writeShort(o),t.writeByte(0);var r=i(2);t.writeByte(2);for(var e=0;255<r.length-e;)t.writeByte(255),t.writeBytes(r,e,255),e+=255;t.writeByte(r.length-e),t.writeBytes(r,e,r.length-e),t.writeByte(0),t.writeString(";")}},i=function(t){for(var r=1<<t,e=1+(1<<t),n=t+1,o=h(),i=0;i<r;i+=1)o.add(String.fromCharCode(i));o.add(String.fromCharCode(r)),o.add(String.fromCharCode(e));var a=p(),u=function(t){var e=t,n=0,o=0,r={write:function(t,r){if(t>>>r!=0)throw"length over";for(;8<=n+r;)e.writeByte(255&(t<<n|o)),r-=8-n,t>>>=8-n,n=o=0;o|=t<<n,n+=r},flush:function(){0<n&&e.writeByte(o)}};return r}(a);u.write(r,n);var f=0,c=String.fromCharCode(l[f]);for(f+=1;f<l.length;){var g=String.fromCharCode(l[f]);f+=1,o.contains(c+g)?c+=g:(u.write(o.indexOf(c),n),o.size()<4095&&(o.size()==1<<n&&(n+=1),o.add(c+g)),c=g)}return u.write(o.indexOf(c),n),u.write(e,n),u.flush(),a.toByteArray()},h=function(){var r={},e=0,n={add:function(t){if(n.contains(t))throw"dup key:"+t;r[t]=e,e+=1},size:function(){return e},indexOf:function(t){return r[t]},contains:function(t){return void 0!==r[t]}};return n};return e}(t,r),o=0;o<r;o+=1)for(var i=0;i<t;i+=1)n.setPixel(i,o,e(i,o));var a=p();n.write(a);for(var u=function(){function e(t){a+=String.fromCharCode(r(63&t))}var n=0,o=0,i=0,a="",t={},r=function(t){if(t<0);else{if(t<26)return 65+t;if(t<52)return t-26+97;if(t<62)return t-52+48;if(62==t)return 43;if(63==t)return 47}throw"n:"+t};return t.writeByte=function(t){for(n=n<<8|255&t,o+=8,i+=1;6<=o;)e(n>>>o-6),o-=6},t.flush=function(){if(0<o&&(e(n<<6-o),o=n=0),i%3!=0)for(var t=3-i%3,r=0;r<t;r+=1)a+="="},t.toString=function(){return a},t}(),f=a.toByteArray(),c=0;c<f.length;c+=1)u.writeByte(f[c]);return u.flush(),"data:image/gif;base64,"+u};return i}();qrcode.stringToBytesFuncs["UTF-8"]=function(t){return function(t){for(var r=[],e=0;e<t.length;e++){var n=t.charCodeAt(e);n<128?r.push(n):n<2048?r.push(192|n>>6,128|63&n):n<55296||57344<=n?r.push(224|n>>12,128|n>>6&63,128|63&n):(e++,n=65536+((1023&n)<<10|1023&t.charCodeAt(e)),r.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n))}return r}(t)},function(t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports&&(module.exports=t())}(function(){return qrcode});
|
|
7
|
+
export default qrcode;
|
|
8
|
+
export { qrcode };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentic ISO/IEC 18004 Compliant QR Code Engine (Pure JavaScript).
|
|
3
|
+
* Supports Versions 1 through 40, Error Correction Levels L/M/Q/H.
|
|
4
|
+
* Produces 100% genuine scannable QR codes for all UPI apps and Android Device Pairing.
|
|
5
|
+
*/
|
|
6
|
+
import { qrcode } from './qr-lib.js';
|
|
7
|
+
|
|
8
|
+
export class QrCode {
|
|
9
|
+
/**
|
|
10
|
+
* Generates a 2D binary matrix for the input text.
|
|
11
|
+
* @param {string} text
|
|
12
|
+
* @param {'L'|'M'|'Q'|'H'} [errorCorrection='M']
|
|
13
|
+
* @returns {number[][]} 2D matrix (1 = dark, 0 = light)
|
|
14
|
+
*/
|
|
15
|
+
static encode(text, errorCorrection = 'M') {
|
|
16
|
+
const qr = qrcode(0, errorCorrection);
|
|
17
|
+
qr.addData(text);
|
|
18
|
+
qr.make();
|
|
19
|
+
const count = qr.getModuleCount();
|
|
20
|
+
const matrix = [];
|
|
21
|
+
for (let r = 0; r < count; r++) {
|
|
22
|
+
const row = [];
|
|
23
|
+
for (let c = 0; c < count; c++) {
|
|
24
|
+
row.push(qr.isDark(r, c) ? 1 : 0);
|
|
25
|
+
}
|
|
26
|
+
matrix.push(row);
|
|
27
|
+
}
|
|
28
|
+
return matrix;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generates valid, crisp SVG markup for an authentic QR Code.
|
|
34
|
+
* @param {string} text
|
|
35
|
+
* @param {number} [boxSize=240]
|
|
36
|
+
* @param {'L'|'M'|'Q'|'H'} [errorCorrection='M']
|
|
37
|
+
* @returns {string} SVG markup string
|
|
38
|
+
*/
|
|
39
|
+
export function generateQrSvg(text, boxSize = 240, errorCorrection = 'M') {
|
|
40
|
+
const matrix = QrCode.encode(text, errorCorrection);
|
|
41
|
+
const numModules = matrix.length;
|
|
42
|
+
const moduleSize = boxSize / numModules;
|
|
43
|
+
|
|
44
|
+
let rects = '';
|
|
45
|
+
for (let r = 0; r < numModules; r++) {
|
|
46
|
+
for (let c = 0; c < numModules; c++) {
|
|
47
|
+
if (matrix[r][c] === 1) {
|
|
48
|
+
const x = (c * moduleSize).toFixed(2);
|
|
49
|
+
const y = (r * moduleSize).toFixed(2);
|
|
50
|
+
const s = (moduleSize + 0.05).toFixed(2);
|
|
51
|
+
rects += `<rect x="${x}" y="${y}" width="${s}" height="${s}" fill="#0f172a" />\n`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${boxSize} ${boxSize}" width="100%" height="100%" shape-rendering="crispEdges">
|
|
57
|
+
<rect width="${boxSize}" height="${boxSize}" fill="#ffffff" rx="8" />
|
|
58
|
+
${rects}
|
|
59
|
+
</svg>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const generateQrMatrix = (text, errorCorrection = 'M') => QrCode.encode(text, errorCorrection);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { PaymentStatus } from '../common/index.js';
|
|
2
|
+
|
|
3
|
+
export class QueueEngine {
|
|
4
|
+
/**
|
|
5
|
+
* @param {{
|
|
6
|
+
* sessionRepo: import('../db/index.js').PaymentSessionRepository,
|
|
7
|
+
* accountRepo: import('../db/index.js').ReceivingAccountRepository,
|
|
8
|
+
* eventEmitter?: { emit: (event: string, payload: any) => void },
|
|
9
|
+
* auditRepo?: import('../db/index.js').AuditLogRepository,
|
|
10
|
+
* activeDurationMs?: number
|
|
11
|
+
* }} options
|
|
12
|
+
*/
|
|
13
|
+
constructor(options) {
|
|
14
|
+
// Payment window used whenever a caller does not pass one (PAYMENT_ACTIVE_WINDOW_MS).
|
|
15
|
+
this.activeDurationMs = options.activeDurationMs || 300000;
|
|
16
|
+
// Verification window for a session that reached PROCESSING (PAYMENT_PROCESSING_WINDOW_MS).
|
|
17
|
+
this.processingDurationMs = options.processingDurationMs || 30000;
|
|
18
|
+
this.sessionRepo = options.sessionRepo;
|
|
19
|
+
this.accountRepo = options.accountRepo;
|
|
20
|
+
this.eventEmitter = options.eventEmitter || { emit: () => {} };
|
|
21
|
+
this.auditRepo = options.auditRepo;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Attempts to acquire an active slot or places the session into the queue.
|
|
26
|
+
* @param {string} sessionId
|
|
27
|
+
* @param {string} receivingAccountId
|
|
28
|
+
* @param {number} [activeDurationMs] Defaults to the engine's configured payment window
|
|
29
|
+
* @returns {{ status: string, queuePosition: number, expiresAt: Date | null }}
|
|
30
|
+
*/
|
|
31
|
+
enqueueOrActivate(sessionId, receivingAccountId, activeDurationMs = this.activeDurationMs) {
|
|
32
|
+
const result = this.sessionRepo.acquireSlotAtomic(sessionId, receivingAccountId, activeDurationMs);
|
|
33
|
+
|
|
34
|
+
if (result.status === PaymentStatus.ACTIVE) {
|
|
35
|
+
this.eventEmitter.emit('SESSION.ACTIVATED', {
|
|
36
|
+
sessionId,
|
|
37
|
+
receivingAccountId,
|
|
38
|
+
expiresAt: result.expiresAt
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (this.auditRepo) {
|
|
42
|
+
this.auditRepo.log({
|
|
43
|
+
entityType: 'PAYMENT_SESSION',
|
|
44
|
+
entityId: sessionId,
|
|
45
|
+
action: 'PAYMENT_SLOT_ACTIVATED',
|
|
46
|
+
actorType: 'SYSTEM',
|
|
47
|
+
details: { receivingAccountId, expiresAt: result.expiresAt?.toISOString() }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
this.eventEmitter.emit('SESSION.QUEUED', {
|
|
52
|
+
sessionId,
|
|
53
|
+
receivingAccountId,
|
|
54
|
+
queuePosition: result.queuePosition
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (this.auditRepo) {
|
|
58
|
+
this.auditRepo.log({
|
|
59
|
+
entityType: 'PAYMENT_SESSION',
|
|
60
|
+
entityId: sessionId,
|
|
61
|
+
action: 'PAYMENT_ENQUEUED',
|
|
62
|
+
actorType: 'SYSTEM',
|
|
63
|
+
details: { receivingAccountId, queuePosition: result.queuePosition }
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Promotes the next queued session for a receiving account if the slot is free.
|
|
73
|
+
* @param {string} receivingAccountId
|
|
74
|
+
* @param {number} [activeDurationMs] Defaults to the engine's configured payment window
|
|
75
|
+
* @returns {any | null} The newly activated session or null if queue is empty or slot is busy
|
|
76
|
+
*/
|
|
77
|
+
promoteNext(receivingAccountId, activeDurationMs = this.activeDurationMs) {
|
|
78
|
+
// Check if slot is currently occupied
|
|
79
|
+
const active = this.sessionRepo.findCurrentActiveOrProcessing(receivingAccountId);
|
|
80
|
+
if (active) {
|
|
81
|
+
return null; // Slot is still owned
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const nextQueued = this.sessionRepo.getNextQueuedSession(receivingAccountId);
|
|
85
|
+
if (!nextQueued) {
|
|
86
|
+
return null; // Queue is empty
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Atomically activate the queued session
|
|
90
|
+
const res = this.sessionRepo.acquireSlotAtomic(nextQueued.id, receivingAccountId, activeDurationMs);
|
|
91
|
+
if (res.status === PaymentStatus.ACTIVE) {
|
|
92
|
+
// Re-index remaining queued sessions
|
|
93
|
+
this.sessionRepo.reindexQueue(receivingAccountId);
|
|
94
|
+
|
|
95
|
+
const activatedSession = this.sessionRepo.findById(nextQueued.id);
|
|
96
|
+
|
|
97
|
+
this.eventEmitter.emit('SESSION.ACTIVATED', {
|
|
98
|
+
sessionId: nextQueued.id,
|
|
99
|
+
receivingAccountId,
|
|
100
|
+
expiresAt: activatedSession.expiresAt
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
this.eventEmitter.emit('QUEUE.PROMOTED', {
|
|
104
|
+
receivingAccountId,
|
|
105
|
+
promotedSessionId: nextQueued.id
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (this.auditRepo) {
|
|
109
|
+
this.auditRepo.log({
|
|
110
|
+
merchantId: activatedSession.merchantId,
|
|
111
|
+
entityType: 'PAYMENT_SESSION',
|
|
112
|
+
entityId: nextQueued.id,
|
|
113
|
+
action: 'QUEUE_PROMOTION_TO_ACTIVE',
|
|
114
|
+
actorType: 'SYSTEM',
|
|
115
|
+
details: { receivingAccountId, expiresAt: activatedSession.expiresAt?.toISOString() }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return activatedSession;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Calculates estimated wait time in seconds for a queued session.
|
|
127
|
+
* @param {string} receivingAccountId
|
|
128
|
+
* @param {number} queuePosition
|
|
129
|
+
* @returns {number} Estimated seconds until activation
|
|
130
|
+
*/
|
|
131
|
+
calculateEstimatedWaitSeconds(receivingAccountId, queuePosition) {
|
|
132
|
+
if (queuePosition <= 0) return 0;
|
|
133
|
+
|
|
134
|
+
let currentSlotRemaining = 0;
|
|
135
|
+
const currentActive = this.sessionRepo.findCurrentActiveOrProcessing(receivingAccountId);
|
|
136
|
+
|
|
137
|
+
if (currentActive) {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
if (currentActive.status === PaymentStatus.PROCESSING && currentActive.processingExpiresAt) {
|
|
140
|
+
currentSlotRemaining = Math.max(0, Math.ceil((currentActive.processingExpiresAt.getTime() - now) / 1000));
|
|
141
|
+
} else if (currentActive.status === PaymentStatus.ACTIVE && currentActive.expiresAt) {
|
|
142
|
+
currentSlotRemaining = Math.max(0, Math.ceil((currentActive.expiresAt.getTime() - now) / 1000));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Average per queued cycle: the configured active window plus the processing window.
|
|
147
|
+
// Deriving it keeps the estimate correct when PAYMENT_ACTIVE_WINDOW_MS is changed.
|
|
148
|
+
const CYCLE_DURATION_SECONDS = Math.round((this.activeDurationMs + this.processingDurationMs) / 1000);
|
|
149
|
+
const precedingQueuedWait = (queuePosition - 1) * CYCLE_DURATION_SECONDS;
|
|
150
|
+
|
|
151
|
+
return currentSlotRemaining + precedingQueuedWait;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { PaymentStatus } from '../common/index.js';
|
|
2
|
+
|
|
3
|
+
export class RecoveryEngine {
|
|
4
|
+
/**
|
|
5
|
+
* @param {{
|
|
6
|
+
* db: import('../db/index.js').Database,
|
|
7
|
+
* sessionRepo: import('../db/index.js').PaymentSessionRepository,
|
|
8
|
+
* accountRepo: import('../db/index.js').ReceivingAccountRepository,
|
|
9
|
+
* queueEngine: import('./queue-engine.js').QueueEngine,
|
|
10
|
+
* timingEngine: import('./timing-engine.js').TimingEngine,
|
|
11
|
+
* auditRepo?: import('../db/index.js').AuditLogRepository,
|
|
12
|
+
* eventEmitter?: { emit: (event: string, payload: any) => void }
|
|
13
|
+
* }} options
|
|
14
|
+
*/
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.db = options.db;
|
|
17
|
+
this.sessionRepo = options.sessionRepo;
|
|
18
|
+
this.accountRepo = options.accountRepo;
|
|
19
|
+
this.queueEngine = options.queueEngine;
|
|
20
|
+
this.timingEngine = options.timingEngine;
|
|
21
|
+
this.auditRepo = options.auditRepo;
|
|
22
|
+
this.eventEmitter = options.eventEmitter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Performs full crash-recovery on startup.
|
|
27
|
+
* Reconstructs timers and resolves overdue sessions.
|
|
28
|
+
* @returns {{
|
|
29
|
+
* expiredCount: number,
|
|
30
|
+
* failedCount: number,
|
|
31
|
+
* rescheduledCount: number,
|
|
32
|
+
* promotedCount: number
|
|
33
|
+
* }}
|
|
34
|
+
*/
|
|
35
|
+
recover() {
|
|
36
|
+
let expiredCount = 0;
|
|
37
|
+
let failedCount = 0;
|
|
38
|
+
let rescheduledCount = 0;
|
|
39
|
+
let promotedCount = 0;
|
|
40
|
+
|
|
41
|
+
const now = new Date();
|
|
42
|
+
const nowIso = now.toISOString();
|
|
43
|
+
|
|
44
|
+
// 1. Recover overdue PROCESSING sessions -> mark SUCCESS if verified/attributed, else FAILED
|
|
45
|
+
const overdueProcessing = this.db.query(`
|
|
46
|
+
SELECT id, merchant_id, order_id, receiving_account_id, verification_level, attributed_observation_id FROM payment_sessions
|
|
47
|
+
WHERE status = ? AND processing_expires_at < ?
|
|
48
|
+
`, [PaymentStatus.PROCESSING, nowIso]);
|
|
49
|
+
|
|
50
|
+
for (const row of overdueProcessing) {
|
|
51
|
+
if (row.verification_level >= 1 || row.attributed_observation_id) {
|
|
52
|
+
this.sessionRepo.transitionToTerminal(row.id, PaymentStatus.SUCCESS);
|
|
53
|
+
if (this.auditRepo) {
|
|
54
|
+
this.auditRepo.log({
|
|
55
|
+
entityType: 'PAYMENT_SESSION',
|
|
56
|
+
entityId: row.id,
|
|
57
|
+
action: 'CRASH_RECOVERY_ATTRIBUTED_SUCCESS',
|
|
58
|
+
actorType: 'SYSTEM'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (this.eventEmitter) {
|
|
62
|
+
this.eventEmitter.emit('PAYMENT.SUCCESS', {
|
|
63
|
+
paymentId: row.id,
|
|
64
|
+
orderId: row.order_id,
|
|
65
|
+
merchantId: row.merchant_id
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
this.sessionRepo.transitionToTerminal(row.id, PaymentStatus.FAILED);
|
|
70
|
+
failedCount++;
|
|
71
|
+
if (this.auditRepo) {
|
|
72
|
+
this.auditRepo.log({
|
|
73
|
+
entityType: 'PAYMENT_SESSION',
|
|
74
|
+
entityId: row.id,
|
|
75
|
+
action: 'CRASH_RECOVERY_EXPIRED_PROCESSING',
|
|
76
|
+
actorType: 'SYSTEM'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (this.eventEmitter) {
|
|
80
|
+
this.eventEmitter.emit('PAYMENT.FAILED', {
|
|
81
|
+
paymentId: row.id,
|
|
82
|
+
orderId: row.order_id,
|
|
83
|
+
merchantId: row.merchant_id
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 2. Recover overdue ACTIVE sessions -> mark EXPIRED
|
|
90
|
+
const overdueActive = this.db.query(`
|
|
91
|
+
SELECT id, merchant_id, order_id, receiving_account_id FROM payment_sessions
|
|
92
|
+
WHERE status = ? AND expires_at < ?
|
|
93
|
+
`, [PaymentStatus.ACTIVE, nowIso]);
|
|
94
|
+
|
|
95
|
+
for (const row of overdueActive) {
|
|
96
|
+
this.sessionRepo.transitionToTerminal(row.id, PaymentStatus.EXPIRED);
|
|
97
|
+
expiredCount++;
|
|
98
|
+
if (this.auditRepo) {
|
|
99
|
+
this.auditRepo.log({
|
|
100
|
+
entityType: 'PAYMENT_SESSION',
|
|
101
|
+
entityId: row.id,
|
|
102
|
+
action: 'CRASH_RECOVERY_EXPIRED_ACTIVE',
|
|
103
|
+
actorType: 'SYSTEM'
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (this.eventEmitter) {
|
|
107
|
+
this.eventEmitter.emit('PAYMENT.EXPIRED', {
|
|
108
|
+
paymentId: row.id,
|
|
109
|
+
orderId: row.order_id,
|
|
110
|
+
merchantId: row.merchant_id
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 3. Reschedule ongoing ACTIVE sessions
|
|
116
|
+
const ongoingActive = this.db.query(`
|
|
117
|
+
SELECT id, expires_at FROM payment_sessions
|
|
118
|
+
WHERE status = ? AND expires_at >= ?
|
|
119
|
+
`, [PaymentStatus.ACTIVE, nowIso]);
|
|
120
|
+
|
|
121
|
+
for (const row of ongoingActive) {
|
|
122
|
+
this.timingEngine.startActiveTimer(row.id, new Date(row.expires_at));
|
|
123
|
+
rescheduledCount++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 4. Reschedule ongoing PROCESSING sessions
|
|
127
|
+
const ongoingProcessing = this.db.query(`
|
|
128
|
+
SELECT id, processing_expires_at FROM payment_sessions
|
|
129
|
+
WHERE status = ? AND processing_expires_at >= ?
|
|
130
|
+
`, [PaymentStatus.PROCESSING, nowIso]);
|
|
131
|
+
|
|
132
|
+
for (const row of ongoingProcessing) {
|
|
133
|
+
const remainingMs = Math.max(0, new Date(row.processing_expires_at).getTime() - Date.now());
|
|
134
|
+
const procTimer = setTimeout(() => {
|
|
135
|
+
this.timingEngine.handleProcessingTimerExpiration(row.id);
|
|
136
|
+
}, remainingMs);
|
|
137
|
+
if (procTimer.unref) procTimer.unref();
|
|
138
|
+
this.timingEngine.processingTimers.set(row.id, procTimer);
|
|
139
|
+
rescheduledCount++;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 5. Scan all receiving accounts: promote waiting queue if slot is now vacant
|
|
143
|
+
const accounts = this.db.query(`SELECT id FROM payment_receiving_accounts WHERE is_active = 1`);
|
|
144
|
+
for (const acc of accounts) {
|
|
145
|
+
const promoted = this.queueEngine.promoteNext(acc.id);
|
|
146
|
+
if (promoted) {
|
|
147
|
+
promotedCount++;
|
|
148
|
+
this.timingEngine.startActiveTimer(promoted.id, promoted.expiresAt);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
expiredCount,
|
|
154
|
+
failedCount,
|
|
155
|
+
rescheduledCount,
|
|
156
|
+
promotedCount
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isValidTransition, isTerminalStatus, VALID_TRANSITIONS } from '../common/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The transition graph and helpers live in `../common/index.js` so the database layer can enforce
|
|
5
|
+
* them too (`../db/index.js` cannot import `../core/index.js`). They are re-exported here to keep
|
|
6
|
+
* `../core/index.js` the canonical import site for engine consumers.
|
|
7
|
+
*/
|
|
8
|
+
export { VALID_TRANSITIONS, isValidTransition, isTerminalStatus };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Asserts a valid transition, throwing a detailed domain error if violated.
|
|
12
|
+
* @param {string} currentState
|
|
13
|
+
* @param {string} nextState
|
|
14
|
+
*/
|
|
15
|
+
export function assertValidTransition(currentState, nextState) {
|
|
16
|
+
if (!isValidTransition(currentState, nextState)) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Illegal payment state transition: Cannot move from '${currentState}' to '${nextState}'.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Converts internal payment status to the public contract outcome.
|
|
25
|
+
* Throws if the status is not terminal.
|
|
26
|
+
* @param {string} status
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
export function toPublicOutcome(status) {
|
|
30
|
+
switch (status) {
|
|
31
|
+
case 'SUCCESS':
|
|
32
|
+
return 'SUCCESS';
|
|
33
|
+
case 'FAILED':
|
|
34
|
+
return 'FAILED';
|
|
35
|
+
case 'EXPIRED':
|
|
36
|
+
return 'EXPIRED';
|
|
37
|
+
case 'CANCELLED':
|
|
38
|
+
return 'CANCELLED';
|
|
39
|
+
default:
|
|
40
|
+
throw new Error(`Status '${status}' is an internal non-terminal state and cannot be mapped to a public outcome.`);
|
|
41
|
+
}
|
|
42
|
+
}
|