@opengis/pay 1.2.3 → 1.3.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.
Files changed (61) hide show
  1. package/config.js +7 -0
  2. package/module/pay/pt/marketplace-service-payment-client-template.html +7 -20
  3. package/package.json +15 -31
  4. package/plugin.js +13 -10
  5. package/server/plugins/crud/dataDelete.js +82 -0
  6. package/server/plugins/crud/dataInsert.js +76 -0
  7. package/server/plugins/crud/dataUpdate.js +112 -0
  8. package/server/plugins/{cron.js → hook.js} +24 -22
  9. package/server/plugins/pg/funcs/autoIndex.js +102 -0
  10. package/server/plugins/pg/funcs/getDBParams.js +15 -0
  11. package/server/plugins/pg/funcs/getMeta.js +48 -0
  12. package/server/plugins/pg/funcs/getPG.js +39 -0
  13. package/server/plugins/pg/funcs/getPGAsync.js +40 -0
  14. package/server/plugins/pg/funcs/init.js +113 -0
  15. package/server/plugins/pg/index.js +24 -0
  16. package/server/plugins/pg/pgClients.js +22 -0
  17. package/server/plugins/redis/client.js +8 -0
  18. package/server/plugins/redis/funcs/getRedis.js +24 -0
  19. package/server/plugins/redis/funcs/redisClients.js +3 -0
  20. package/server/plugins/redis/index.js +11 -0
  21. package/server/routes/liqpay/controllers/liqpay.redirect.js +7 -11
  22. package/server/routes/liqpay/controllers/liqpay.status.js +26 -15
  23. package/server/routes/liqpay/utils/check.status.js +1 -1
  24. package/server/routes/monopay/controllers/monopay.redirect.js +141 -0
  25. package/server/routes/monopay/controllers/monopay.status.js +70 -0
  26. package/server/routes/monopay/index.mjs +32 -0
  27. package/server/routes/portmone/controllers/payment.info.js +9 -9
  28. package/server/routes/portmone/controllers/portmone.redirect.js +23 -17
  29. package/server/routes/portmone/controllers/portmone.status.js +49 -38
  30. package/server/routes/portmone/utils/portmone.check.status.js +34 -33
  31. package/server/utils/cron/addCron.js +48 -0
  32. package/server/utils/cron/cronList.js +1 -0
  33. package/server/utils/cron/interval2ms.js +40 -0
  34. package/server/utils/cron/runCron.js +24 -0
  35. package/server/utils/cron/verifyUnique.js +23 -0
  36. package/server/utils/getFolder.js +11 -0
  37. package/server/utils/getInsertQuery.js +44 -0
  38. package/server/utils/migration/exec.migrations.js +38 -0
  39. package/server/utils/migration/exec.sql.js +48 -0
  40. package/server/utils/sendNotification.js +69 -0
  41. package/module/pay1/card/billing.accounts.table/general_info.hbs +0 -11
  42. package/module/pay1/card/billing.accounts.table/index.yml +0 -15
  43. package/module/pay1/card/billing.accounts.table/users.hbs +0 -12
  44. package/module/pay1/card/billing.payments.table/general_info.hbs +0 -13
  45. package/module/pay1/card/billing.payments.table/index.yml +0 -10
  46. package/module/pay1/card/billing.users.table/general_info.hbs +0 -11
  47. package/module/pay1/card/billing.users.table/index.yml +0 -10
  48. package/module/pay1/cls/billing.payment_status.json +0 -22
  49. package/module/pay1/form/billing.account_user.form.json +0 -15
  50. package/module/pay1/form/billing.accounts.form.json +0 -31
  51. package/module/pay1/form/billing.payment.form.json +0 -23
  52. package/module/pay1/form/billing.users.form.json +0 -39
  53. package/module/pay1/menu.json +0 -24
  54. package/module/pay1/pt/marketplace-service-payment-client-template.html +0 -147
  55. package/module/pay1/select/billing.account_id.sql +0 -1
  56. package/module/pay1/select/billing.service_id.sql +0 -1
  57. package/module/pay1/select/billing.subscription_id.sql +0 -1
  58. package/module/pay1/select/billing.user_id.sql +0 -1
  59. package/module/pay1/table/billing.accounts.table.json +0 -61
  60. package/module/pay1/table/billing.payments.table.json +0 -68
  61. package/module/pay1/table/billing.users.table.json +0 -71
@@ -0,0 +1,39 @@
1
+ import pg from 'pg';
2
+
3
+ const { types } = pg;
4
+ types.setTypeParser(1082, (stringValue) => stringValue);
5
+ types.setTypeParser(1114, (stringValue) => stringValue);
6
+
7
+ import config from '../../../../config.js';
8
+ import pgClients from '../pgClients.js';
9
+ import init from './init.js';
10
+ import getDBParams from './getDBParams.js';
11
+
12
+ function getPG(param) {
13
+ if (!config.pg) return null;
14
+ const {
15
+ user, password, host, port, db, database, name: origin,
16
+ } = (typeof param === 'string' ? getDBParams(param) : param || {});
17
+ const name = origin || db || database || param || 'client';
18
+ if (pgClients[name]) return pgClients[name];
19
+
20
+ const dbConfig = {
21
+ user: user || config.pg?.user || 'postgres',
22
+ password: password || config.pg?.password || 'postgres',
23
+ host: host || config.pg?.host,
24
+ port: port || config.pg?.port,
25
+ database: db || database || config.pg?.db || config.pg?.database,
26
+ statement_timeout: config.pg?.statement_timeout || 10000,
27
+ };
28
+
29
+ if (!dbConfig.database) { return null; }
30
+
31
+ pgClients[name] = new pg.Pool(dbConfig);
32
+ pgClients[name].init = async () => {
33
+ await init(pgClients[name]);
34
+ };
35
+ init(pgClients[name]);
36
+ return pgClients[name];
37
+ }
38
+
39
+ export default getPG;
@@ -0,0 +1,40 @@
1
+ import pg from 'pg';
2
+
3
+ const { types } = pg;
4
+ types.setTypeParser(1082, (stringValue) => stringValue);
5
+ types.setTypeParser(1114, (stringValue) => stringValue);
6
+
7
+ import config from '../../../../config.js';
8
+ import pgClients from '../pgClients.js';
9
+ import init from './init.js';
10
+ import getDBParams from './getDBParams.js';
11
+
12
+ async function getPGAsync(param) {
13
+ if (!config.pg) return null;
14
+
15
+ const {
16
+ user, password, host, port, db, database, name: origin,
17
+ } = (typeof param === 'string' ? getDBParams(param) : param || {});
18
+ const name = origin || db || database || param || 'client';
19
+
20
+ if (pgClients[name]?.tlist) return pgClients[name];
21
+
22
+ const dbConfig = {
23
+ user: user || config.pg?.user || 'postgres',
24
+ password: password || config.pg?.password || 'postgres',
25
+ host: host || config.pg?.host,
26
+ port: port || config.pg?.port,
27
+ database: db || database || config.pg?.db || config.pg?.database,
28
+ statement_timeout: config.pg?.statement_timeout || 10000,
29
+ };
30
+
31
+ if (!dbConfig.database) { return null; }
32
+
33
+ pgClients[name] = new pg.Pool(dbConfig);
34
+
35
+ await init(pgClients[name]);
36
+
37
+ return pgClients[name];
38
+ }
39
+
40
+ export default getPGAsync;
@@ -0,0 +1,113 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import config from '../../../../config.js';
4
+ import getRedis from '../../redis/funcs/getRedis.js';
5
+
6
+ const rclient = getRedis({ db: 0 });
7
+
8
+ async function init(client) {
9
+ if (!client?.options?.database) { return; }
10
+
11
+ const textQuery = `select
12
+ (select json_object_agg(conrelid::regclass ,(SELECT attname FROM pg_attribute WHERE attrelid = c.conrelid and attnum = c.conkey[1]))
13
+ from pg_constraint c where contype='p' and connamespace::regnamespace::text not in ('sde')) as pk,
14
+ (SELECT json_object_agg(t.oid::text,pg_catalog.format_type(t.oid, NULL)) FROM pg_catalog.pg_type t) as "pgType"`;
15
+ const { pgType, pk } = await client.query(textQuery).then((d) => d.rows[0]);
16
+
17
+ const tlist = await client.query(`select array_agg((select nspname from pg_namespace where oid=relnamespace)||'.'||relname) tlist
18
+ from pg_class where relkind in ('r','v')`).then((d) => d.rows[0].tlist);
19
+
20
+ const { rows = [] } = await client.query(`select (select nspname from pg_namespace where oid=relnamespace)||'.'||relname as tname, relkind
21
+ from pg_class where relkind in ('r','v')`);
22
+ const relkinds = rows.reduce((acc, curr) => Object.assign(acc, { [curr.tname]: curr.relkind }), {});
23
+
24
+ async function query(q, args = [], isstream = false) {
25
+ try {
26
+ if (isstream) {
27
+ await client.query('set statement_timeout to 100000000');
28
+ }
29
+ const data = await client.query(q, args);
30
+ await client.query('set statement_timeout to 0');
31
+ return data;
32
+ }
33
+ catch (err) {
34
+ await client.query('set statement_timeout to 0');
35
+ if (err.message === 'canceling statement due to statement timeout') {
36
+ console.warn('timeout/query', q, err.stack);
37
+ return { rows: [], timeout: true };
38
+ }
39
+ throw new Error(err);
40
+ }
41
+ }
42
+
43
+ async function querySafe(q, param = {}) {
44
+ const { args, isstream } = param;
45
+ const data = await query(q, args, isstream);
46
+ return data;
47
+ }
48
+
49
+ async function one(q, param = {}) {
50
+ const data = await query(q, Array.isArray(param) ? param : param.args || []);
51
+ const result = ((Array.isArray(data) ? data.pop() : data)?.rows || [])[0] || {};
52
+ return result;
53
+ }
54
+
55
+ async function queryNotice(q, args = [], cb = () => { }) {
56
+ const clientCb = await client.connect();
57
+ clientCb.on('notice', (e) => {
58
+ cb(e.message);
59
+ });
60
+ let result;
61
+ try {
62
+ result = await clientCb.query(q, args);
63
+ clientCb.end();
64
+ }
65
+ catch (err) {
66
+ clientCb.end();
67
+ cb(err.toString(), 1);
68
+ throw err;
69
+ }
70
+ return result;
71
+ }
72
+
73
+ async function queryCache(q, param = {}) {
74
+ const { table, args = [], time = 15 } = param;
75
+ const seconds = typeof time !== 'number' || time < 0 ? 0 : time * 60;
76
+
77
+ if (seconds === 0 || config.disableCache) {
78
+ const data = await query(q, args || []);
79
+ return data;
80
+ }
81
+
82
+ // CRUD table state
83
+ const keyCacheTable = `pg:${table}:crud`;
84
+ const crudInc = table && config.redis ? (await rclient.get(keyCacheTable) || 0) : 0;
85
+
86
+ //
87
+ const hash = createHash('sha1').update([q, JSON.stringify(args)].join()).digest('base64');
88
+ const keyCache = `pg:${hash}:${crudInc}`;
89
+
90
+ const cacheData = config.redis ? await rclient.get(keyCache) : null;
91
+
92
+ if (cacheData && !config.local) {
93
+ // console.log('from cache', table, query);
94
+ return JSON.parse(cacheData);
95
+ }
96
+
97
+ const data = await query(q, args || []);
98
+
99
+ if (seconds > 0 && config.redis) {
100
+ rclient.set(keyCache, JSON.stringify(data), 'EX', seconds);
101
+ }
102
+
103
+ // console.log('no cache', table, crudInc, query);
104
+ return data;
105
+ }
106
+
107
+ Object.assign(client, {
108
+ one, pgType, pk, tlist, relkinds, queryCache, queryNotice, querySafe,
109
+ });
110
+ }
111
+
112
+ // export default client;
113
+ export default init;
@@ -0,0 +1,24 @@
1
+ import pgClients from './pgClients.js';
2
+ import getPGAsync from './funcs/getPGAsync.js';
3
+
4
+ function close() {
5
+ Object.keys(pgClients).forEach((el) => {
6
+ pgClients[el].end();
7
+ });
8
+ }
9
+
10
+ export default async function plugin(app, config) {
11
+ const client = await getPGAsync({ ...config.pg || {}, name: 'client' });
12
+
13
+ app.addHook('onRequest', async (req) => {
14
+ req.pg = req.pg || client;
15
+ });
16
+
17
+ app.addHook('onError', async (req, reply, err) => {
18
+ if (err.message === 'canceling statement due to statement timeout') {
19
+ console.warn('request timeout', req.method, req.url, req.headers?.referer, err.stack);
20
+ }
21
+ });
22
+
23
+ app.addHook('onClose', close);
24
+ }
@@ -0,0 +1,22 @@
1
+ import pg from 'pg';
2
+
3
+ import config from '../../../config.js';
4
+ import init from './funcs/init.js';
5
+
6
+ const pgClients = {};
7
+ if (config.pg) {
8
+ const client = new pg.Pool({
9
+ host: config.pg?.host || '127.0.0.1',
10
+ port: config.pg?.port || 5432,
11
+ database: config.pg?.database || 'postgres',
12
+ user: config.pg?.user || 'postgres',
13
+ password: config.pg?.password || 'postgres',
14
+ statement_timeout: config.pg?.statement_timeout || 10000,
15
+ });
16
+ client.init = async () => {
17
+ await init(client);
18
+ };
19
+ client.init();
20
+ pgClients.client = client;
21
+ }
22
+ export default pgClients;
@@ -0,0 +1,8 @@
1
+ import redisClients from './funcs/redisClients.js';
2
+ import getRedis from './funcs/getRedis.js';
3
+
4
+ if (!redisClients[0]) {
5
+ getRedis({ db: 0 });
6
+ }
7
+
8
+ export default redisClients[0];
@@ -0,0 +1,24 @@
1
+ import Redis from 'ioredis';
2
+
3
+ import config from '../../../../config.js';
4
+ import redisClients from './redisClients.js';
5
+
6
+ function getRedis({ db } = { db: 0 }) {
7
+ if (!config.redis) return null;
8
+ if (redisClients[db]) return redisClients[db];
9
+
10
+ const redisConfig = {
11
+ db,
12
+ keyPrefix: `${config.db}:`,
13
+ host: config.redis?.host || '127.0.0.1',
14
+ port: config.redis?.port || 6379, // Redis port
15
+ family: 4, // 4 (IPv4) or 6 (IPv6)
16
+ closeClient: true,
17
+ };
18
+
19
+ redisClients[db] = new Redis(redisConfig);
20
+
21
+ return redisClients[db];
22
+ }
23
+
24
+ export default getRedis;
@@ -0,0 +1,3 @@
1
+ const redisClients = {};
2
+
3
+ export default redisClients;
@@ -0,0 +1,11 @@
1
+ import redisClients from './funcs/redisClients.js';
2
+
3
+ function close() {
4
+ Object.keys(redisClients).forEach((key) => redisClients[key].quit());
5
+ }
6
+
7
+ async function plugin(fastify) {
8
+ fastify.addHook('onClose', close);
9
+ }
10
+
11
+ export default plugin;
@@ -1,9 +1,10 @@
1
1
  import request from 'request-promise';
2
2
  import LiqPay from 'liqpayjs-sdk';
3
3
 
4
- import {
5
- config, logger, pgClients, dataInsert, dataUpdate, handlebarsSync,
6
- } from '@opengis/fastify-table/utils.js';
4
+ import config from '../../../../config.js';
5
+ import pgClients from '../../../plugins/pg/pgClients.js';
6
+ import dataInsert from '../../../plugins/crud/dataInsert.js';
7
+ import dataUpdate from '../../../plugins/crud/dataUpdate.js';
7
8
 
8
9
  const { prefix = '/api' } = config;
9
10
 
@@ -64,7 +65,7 @@ export default async function liqpayRedirect(req, reply) {
64
65
  table: 'billing.payments',
65
66
  id: paymentId,
66
67
  data: {
67
- referer_url: paymentRedirect ? handlebarsSync.compile(paymentRedirect)({ id: paymentId }) : headers?.referer,
68
+ referer_url: paymentRedirect ? paymentRedirect.replace(/{{id}}/g, paymentId) : headers?.referer,
68
69
  payment_status: 'inprogress',
69
70
  },
70
71
  uid: user?.uid || '0',
@@ -73,10 +74,6 @@ export default async function liqpayRedirect(req, reply) {
73
74
  const protocol = headers?.referer?.split?.('://')?.shift?.() || req.protocol || 'https';
74
75
  const rootPageUrl = `${protocol || 'https'}://${domain}`;
75
76
 
76
- /* const resultUrl = paymentRedirect
77
- ? `${rootPageUrl}${handlebarsSync.compile(paymentRedirect)({ id: paymentId })}`
78
- : headers?.referer || `${rootPageUrl}`; */
79
-
80
77
  // redirect to api to check payment status and redirect user to page afterwards
81
78
  const serverUrl = `${rootPageUrl}${prefix}/liqpay/${paymentId}/success`;
82
79
 
@@ -130,12 +127,11 @@ export default async function liqpayRedirect(req, reply) {
130
127
  }
131
128
 
132
129
  Object.assign(logObj, { paymentId, redirect: response.headers?.location, status: 302 });
133
- logger.file('payments', logObj);
130
+ console.log('payments/info', JSON.stringify(logObj));
134
131
  return reply.redirect(response.headers.location);
135
132
  }
136
133
  catch (err) {
137
- Object.assign(logObj, { error: err.toString() });
138
- logger.file('payments/error', logObj);
134
+ console.error('payments/error', JSON.stringify(logObj), err.toString(), err.stack);
139
135
  return reply.status(500).send(err.toString());
140
136
  }
141
137
  }
@@ -1,13 +1,19 @@
1
1
  import LiqPay from 'liqpayjs-sdk';
2
+ import path from 'node:path';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'url';
2
5
 
3
- import {
4
- config, logger, pgClients, dataUpdate, getTemplate,
5
- } from '@opengis/fastify-table/utils.js';
6
+ import config from '../../../../config.js';
7
+ import pgClients from '../../../plugins/pg/pgClients.js';
8
+ import dataUpdate from '../../../plugins/crud/dataUpdate.js';
6
9
 
7
- import { sendNotification } from '@opengis/admin/utils.js';
10
+ import sendNotification from '../../../utils/sendNotification.js';
8
11
 
9
12
  import checkStatus from '../utils/check.status.js';
10
13
 
14
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const ptBody = readFileSync(path.join(dirname, '../../../../module/pay/pt/marketplace-service-payment-client-template.html'));
16
+
11
17
  const newDate = () => {
12
18
  const date = new Date();
13
19
  const tzOffset = date.getTimezoneOffset();
@@ -72,14 +78,14 @@ export default async function liqpayStatus(req, reply) {
72
78
  );
73
79
 
74
80
  if (body.signature !== sign) {
75
- logger.file('payments/warn', {
81
+ console.warn('payments/warn', JSON.stringify({
76
82
  type: 'status',
77
83
  service: 'liqpay',
78
84
  paymentId,
79
85
  origin,
80
86
  uid: user?.uid || '0',
81
87
  message: 'Transaction signature does not match',
82
- });
88
+ }));
83
89
  return reply.status(403).send('access restricted: signature mismatch');
84
90
  }
85
91
  }
@@ -97,14 +103,14 @@ export default async function liqpayStatus(req, reply) {
97
103
  } = data;
98
104
 
99
105
  if (trxStatus === 'success') {
100
- logger.file('payments/skip', {
106
+ console.info('payments/skip', JSON.stringify({
101
107
  type: 'status',
102
108
  service: 'liqpay',
103
109
  paymentId,
104
110
  origin,
105
111
  uid: user?.uid || '0',
106
112
  message: 'Transaction was already processed',
107
- });
113
+ }));
108
114
  return reply.status(400).send('Transaction was already processed');
109
115
  }
110
116
 
@@ -116,7 +122,7 @@ export default async function liqpayStatus(req, reply) {
116
122
  return reply.status(400).send('Payment not registered via liqpay service');
117
123
  }
118
124
 
119
- logger.file('payments', {
125
+ console.log('payments/info', JSON.stringify({
120
126
  type: 'status',
121
127
  service: 'liqpay',
122
128
  paymentId,
@@ -125,7 +131,7 @@ export default async function liqpayStatus(req, reply) {
125
131
  refill: refillSum,
126
132
  body,
127
133
  params,
128
- });
134
+ }));
129
135
 
130
136
  // last trx contains total balance
131
137
  const { total_balance: totalBalance = 0 } = await pg.query(`select total_balance from billing.payments
@@ -153,7 +159,7 @@ export default async function liqpayStatus(req, reply) {
153
159
  const { account_email: to } = await pg.query('select account_email from billing.accounts where account_id=$1', [accountId])
154
160
  .then(el => el.rows?.[0] || {});
155
161
 
156
- logger.file('payments', {
162
+ console.log('payments/info2', JSON.stringify({
157
163
  type: 'status',
158
164
  service: 'liqpay',
159
165
  paymentId,
@@ -162,7 +168,7 @@ export default async function liqpayStatus(req, reply) {
162
168
  origin,
163
169
  redirect: redirectUrl,
164
170
  uid: user.uid || '0',
165
- });
171
+ }));
166
172
 
167
173
  Object.assign(data, {
168
174
  domain: req.hostname,
@@ -170,13 +176,18 @@ export default async function liqpayStatus(req, reply) {
170
176
  });
171
177
 
172
178
  if (to) {
173
- const pt = await getTemplate('pt', 'marketplace-service-payment-client-template');
179
+ const pt = ptBody
180
+ .replace(/{{domain}}/g, req.hostname)
181
+ .replace(/{{total_balance}}/g, +(refillSum || 0) + (+totalBalance || 0))
182
+ .replace(/{{username}}/g, to)
183
+ .replace(/{{created_at}}/g, (data?.cdate || data?.created_at || new Date()).toLocaleString('uk-UA'))
184
+ .replace(/{{payment_status}}/g, responseStatus)
185
+ .replace(/{{payment_amount}}/g, refillSum || 0);
174
186
  const options = {
175
187
  pg,
176
188
  to,
177
189
  title: `Поповнення рахунку на порталі ${req.hostname}`,
178
- template: pt?.html || pt,
179
- data,
190
+ pt,
180
191
  nocache: config?.local,
181
192
  };
182
193
  await sendNotification(options);
@@ -1,7 +1,7 @@
1
1
  import LiqPay from 'liqpayjs-sdk';
2
2
  import request from 'request-promise';
3
3
 
4
- import { config } from '@opengis/fastify-table/utils.js';
4
+ import config from '../../../../config.js';
5
5
 
6
6
  const { publicKey, privateKey } = config.integrations?.liqpay || {};
7
7
 
@@ -0,0 +1,141 @@
1
+ import config from '../../../../config.js';
2
+ import pgClients from '../../../plugins/pg/pgClients.js';
3
+ import dataInsert from '../../../plugins/crud/dataInsert.js';
4
+ import dataUpdate from '../../../plugins/crud/dataUpdate.js';
5
+
6
+ const { prefix = '/api' } = config;
7
+ const {
8
+ host = 'https://api.monobank.ua', token, paymentRedirect, merchantPaymInfo,
9
+ } = config.integrations?.monopay || {};
10
+
11
+ // http://billing.local.ua/api/monopay-redirect?price=50
12
+
13
+ export default async function monopayRedirect(req, reply) {
14
+ const {
15
+ pg = pgClients.client, query = {}, user = {}, headers = {},
16
+ } = req;
17
+
18
+ if (!token) {
19
+ return reply.status(400).send('invalid monopay integration settings');
20
+ }
21
+
22
+ const domain = req.hostname.split(':').shift();
23
+
24
+ const paymentData = query.id ? await pg.query('select payment_id, account_id, payment_amount, payment_status from billing.payments where payment_id=$1', [query.id])
25
+ .then(el => el.rows?.[0] || {}) : {};
26
+
27
+ if (paymentData.payment_status && paymentData.payment_status === 'success') {
28
+ return reply.status(400).send('already processed');
29
+ }
30
+
31
+ const paymentAmount = paymentData.payment_amount || query.price;
32
+
33
+ if (!paymentAmount) {
34
+ return reply.status(400).send('price is required');
35
+ }
36
+
37
+ const accountId = paymentData.account_id
38
+ || (user?.uid ? await pg.query('select account_id from billing.account_user where user_id=$1 limit 1', [user?.uid]).then(el => el.rows?.[0]?.account_id) : null);
39
+
40
+ if (!accountId) {
41
+ return reply.status(400).send('account_id is required');
42
+ }
43
+
44
+ const { payment_id: paymentId } = !paymentData.payment_id
45
+ ? await dataInsert({
46
+ table: 'billing.payments',
47
+ data: {
48
+ payment_amount: paymentAmount,
49
+ account_id: accountId,
50
+ payment_service: 'monopay',
51
+ payment_status: 'inprogress',
52
+ },
53
+ uid: user?.uid || '0',
54
+ }).then(el => el.rows?.[0] || {})
55
+ : { payment_id: paymentData.payment_id };
56
+
57
+ if (!paymentId) {
58
+ return { message: 'payment not found', status: 404 };
59
+ }
60
+
61
+ await dataUpdate({
62
+ table: 'billing.payments',
63
+ id: paymentId,
64
+ data: {
65
+ referer_url: paymentRedirect ? paymentRedirect.replace(/{{id}}/g, paymentId) : headers?.referer,
66
+ payment_status: 'inprogress',
67
+ payment_service: 'monopay',
68
+ },
69
+ uid: user?.uid || '0',
70
+ });
71
+
72
+ const protocol = headers?.referer?.split?.('://')?.shift?.() || req.protocol || 'https';
73
+ const rootPageUrl = `${protocol || 'https'}://${domain}`;
74
+
75
+ // redirect to api to check payment status and redirect user to page afterwards
76
+ const serverUrl = `${rootPageUrl}${prefix}/monopay/${paymentId}/success`;
77
+
78
+ const paymentOptions = {
79
+ amount: paymentAmount * 100, // price=1 === 0.01 uah
80
+ ccy: 980, // ISO 4217, 980 = UAH
81
+ merchantPaymInfo,
82
+ redirectUrl: serverUrl,
83
+ // redirectUrl: (paymentRedirect ? `${rootPageUrl}${paymentRedirect.replace(/{{id}}/g, paymentId)}` : headers?.referer) || rootPageUrl,
84
+ webHookUrl: serverUrl,
85
+ validity: 86400, // 1 day by default, then invalid
86
+ paymentType: 'debit',
87
+ // displayType: 'iframe', // default = null > url to mono
88
+ };
89
+
90
+ const logObj = {
91
+ type: 'redirect',
92
+ paymentId,
93
+ accountId,
94
+ price: paymentAmount,
95
+ service: 'monopay',
96
+ domain,
97
+ paymentRedirectUrl: serverUrl,
98
+ uid: user?.uid || '0',
99
+ };
100
+
101
+ try {
102
+ // create payment, get redirect url
103
+ const resp = await fetch(`${host}/api/merchant/invoice/create`, {
104
+ method: 'POST',
105
+ headers: { 'x-token': token },
106
+ body: JSON.stringify(paymentOptions),
107
+ signal: AbortSignal.timeout(5000),
108
+ });
109
+
110
+ const contentType = resp.headers.get('Content-Type');
111
+
112
+ const body = contentType.includes('application/json') && resp.status === 200
113
+ ? await resp.json()
114
+ : await resp.text();
115
+
116
+ if (query.debug) {
117
+ return body;
118
+ }
119
+
120
+ await dataUpdate({
121
+ table: 'billing.payments',
122
+ id: paymentId,
123
+ data: {
124
+ payment_data: resp.status === 200 ? body : { response: body },
125
+ },
126
+ uid: user?.uid || '0',
127
+ });
128
+
129
+ console.log('payments/info', JSON.stringify({ ...logObj, response: body }));
130
+
131
+ if (!body.pageUrl) {
132
+ return reply.status(501).send('monopay service temporary unavailable');
133
+ }
134
+
135
+ return reply.redirect(body.pageUrl);
136
+ }
137
+ catch (err) {
138
+ console.error('payments/error', JSON.stringify(logObj), err.toString(), err.stack);
139
+ return reply.status(500).send(err.toString());
140
+ }
141
+ }
@@ -0,0 +1,70 @@
1
+ import config from '../../../../config.js';
2
+ import pgClients from '../../../plugins/pg/pgClients.js';
3
+ import dataUpdate from '../../../plugins/crud/dataUpdate.js';
4
+
5
+ const {
6
+ host = 'https://api.monobank.ua',
7
+ token,
8
+ } = config.integrations?.monopay || {};
9
+
10
+ const timeoutMs = 5000;
11
+
12
+ export default async function monopayStatus({
13
+ pg = pgClients.client, params = {}, user = {},
14
+ }, reply) {
15
+ const { id, status = 'success' } = params;
16
+
17
+ if (!id) {
18
+ return reply.status(400).send('not enough params: id');
19
+ }
20
+
21
+ const {
22
+ paymentId, refererUrl, paymentStatus, invoiceId, accountId,
23
+ } = await pg.query('select payment_id as "paymentId", account_id as "accountId", payment_status as "paymentStatus", referer_url as "refererUrl", payment_data->>\'invoiceId\' as "invoiceId" from billing.payments where payment_id=$1', [id])
24
+ .then(el => el.rows?.[0] || {});
25
+
26
+ if (!paymentId) {
27
+ return reply.status(404).send('payment not found');
28
+ }
29
+ if (!invoiceId) {
30
+ return reply.status(404).send('invoice not found');
31
+ }
32
+
33
+ if (['success', 'error'].includes(paymentStatus)) {
34
+ return reply.status(404).send('payment already processed');
35
+ }
36
+
37
+ if (status !== 'success') {
38
+ return reply.redirect(refererUrl);
39
+ }
40
+
41
+ const resp = await fetch(`${host}/api/merchant/invoice/status?invoiceId=${invoiceId}`, {
42
+ method: 'GET',
43
+ headers: { 'x-token': token },
44
+ signal: AbortSignal.timeout(timeoutMs),
45
+ }).catch(err => reply.status(501).send('monopay request timeout'));
46
+
47
+ const contentType = resp.headers.get('Content-Type');
48
+
49
+ const body = contentType.includes('application/json') && resp.status === 200
50
+ ? await resp.json()
51
+ : await resp.text();
52
+
53
+ const totalBalanceBefore = await pg.query(`select total_balance from billing.payments
54
+ where account_id=$1 and payment_status in ('success', 'error') and total_balance is not null order by cdate desc limit 1`, [accountId])
55
+ .then(el => el.rows?.[0]?.total_balance || 0);
56
+
57
+ await dataUpdate({
58
+ table: 'billing.payments',
59
+ id: paymentId,
60
+ data: {
61
+ total_balance: (body.amount / 100) + +totalBalanceBefore,
62
+ success_date: new Date(), // newDate(),
63
+ payment_status: body?.status || 'error',
64
+ payment_data: resp.status === 200 ? body : { response: body },
65
+ },
66
+ uid: user?.uid || '0',
67
+ });
68
+
69
+ return reply.redirect(refererUrl || '/');
70
+ }