elpaypg 0.0.0-stage → 2.0.1

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.
Files changed (90) hide show
  1. package/.env.example +8 -0
  2. package/README.md +176 -2
  3. package/package.json +21 -4
  4. package/public/elpay-checkout.js +769 -0
  5. package/public/elpay-sms-gateway.apk +0 -0
  6. package/public/elpay-sms-gateway.apk.sha256 +1 -0
  7. package/public/elpay.js +785 -0
  8. package/public/fonts/dm-sans.woff2 +0 -0
  9. package/public/fonts/sora.woff2 +0 -0
  10. package/public/views/checkout.html +2839 -0
  11. package/public/views/partials/auth.html +110 -0
  12. package/public/views/partials/footer.html +54 -0
  13. package/public/views/partials/header.html +101 -0
  14. package/public/views/partials/modals.html +179 -0
  15. package/public/views/partials/payment-methods.html +526 -0
  16. package/public/views/partials/sidebar.html +75 -0
  17. package/public/views/partials/splash.html +35 -0
  18. package/src/__tests__/smoke.test.js +8 -0
  19. package/src/auth-guard.js +95 -0
  20. package/src/index.js +137 -0
  21. package/src/router.js +46 -0
  22. package/src/routes/admin.routes.js +2424 -0
  23. package/src/routes/checkout.routes.js +2000 -0
  24. package/src/routes/customer.routes.js +650 -0
  25. package/src/routes/device.routes.js +303 -0
  26. package/src/routes/payments.routes.js +446 -0
  27. package/src/routes/pollers.routes.js +653 -0
  28. package/src/services/observer-poller.service.js +369 -0
  29. package/src/services/tunnel-manager.js +130 -0
  30. package/src/services/whatsapp-berry.service.js +743 -0
  31. package/src/vendor/common/__tests__/common.test.js +297 -0
  32. package/src/vendor/common/__tests__/common.test.ts +160 -0
  33. package/src/vendor/common/contracts.js +64 -0
  34. package/src/vendor/common/contracts.ts +188 -0
  35. package/src/vendor/common/crypto.js +132 -0
  36. package/src/vendor/common/crypto.ts +112 -0
  37. package/src/vendor/common/enums.js +91 -0
  38. package/src/vendor/common/enums.ts +91 -0
  39. package/src/vendor/common/index.js +12 -0
  40. package/src/vendor/common/index.ts +8 -0
  41. package/src/vendor/common/money.js +62 -0
  42. package/src/vendor/common/money.ts +56 -0
  43. package/src/vendor/common/observation.js +27 -0
  44. package/src/vendor/common/observation.ts +52 -0
  45. package/src/vendor/common/runtime-config.js +246 -0
  46. package/src/vendor/common/secrets.js +374 -0
  47. package/src/vendor/common/state-machine.d.ts +1 -0
  48. package/src/vendor/common/state-machine.js +1 -0
  49. package/src/vendor/common/state-transitions.js +85 -0
  50. package/src/vendor/common/tunnel-command.js +59 -0
  51. package/src/vendor/common/upi-providers.js +389 -0
  52. package/src/vendor/common/upi-providers.ts +400 -0
  53. package/src/vendor/common/upi.js +69 -0
  54. package/src/vendor/common/upi.ts +61 -0
  55. package/src/vendor/core/__tests__/core.test.js +683 -0
  56. package/src/vendor/core/__tests__/queue-timing.test.js +237 -0
  57. package/src/vendor/core/__tests__/webhook-engine.test.js +243 -0
  58. package/src/vendor/core/attribution-engine.js +855 -0
  59. package/src/vendor/core/bank-email-parser.js +105 -0
  60. package/src/vendor/core/bank-sms-parser.js +183 -0
  61. package/src/vendor/core/device-security.js +506 -0
  62. package/src/vendor/core/index.js +13 -0
  63. package/src/vendor/core/normalizer.js +165 -0
  64. package/src/vendor/core/qr-lib.js +8 -0
  65. package/src/vendor/core/qr.js +62 -0
  66. package/src/vendor/core/queue-engine.js +153 -0
  67. package/src/vendor/core/recovery-engine.js +159 -0
  68. package/src/vendor/core/state-machine.js +42 -0
  69. package/src/vendor/core/timing-engine.js +272 -0
  70. package/src/vendor/core/verification-pipeline.js +1164 -0
  71. package/src/vendor/core/webhook-engine.js +280 -0
  72. package/src/vendor/db/__tests__/auth-matrix.test.js +564 -0
  73. package/src/vendor/db/__tests__/customer-reset.test.js +95 -0
  74. package/src/vendor/db/__tests__/db.test.js +265 -0
  75. package/src/vendor/db/__tests__/schema-parity.test.js +137 -0
  76. package/src/vendor/db/__tests__/sqlite_verify.test.js +15 -0
  77. package/src/vendor/db/database.js +181 -0
  78. package/src/vendor/db/index.js +12 -0
  79. package/src/vendor/db/migrate.js +210 -0
  80. package/src/vendor/db/repositories/audit-log.repository.js +68 -0
  81. package/src/vendor/db/repositories/customer-session.repository.js +181 -0
  82. package/src/vendor/db/repositories/customer.repository.js +636 -0
  83. package/src/vendor/db/repositories/device.repository.js +285 -0
  84. package/src/vendor/db/repositories/merchant.repository.js +158 -0
  85. package/src/vendor/db/repositories/observation.repository.js +124 -0
  86. package/src/vendor/db/repositories/payment-session.repository.js +618 -0
  87. package/src/vendor/db/repositories/receiving-account.repository.js +244 -0
  88. package/src/vendor/db/repositories/support-ticket.repository.js +140 -0
  89. package/src/vendor/db/schema.sql +266 -0
  90. 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+="&lt;";break;case">":r+="&gt;";break;case"&":r+="&amp;";break;case'"':r+="&quot;";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
+ }