@opengis/pay 1.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/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # settings
2
+
3
+ [![NPM version](https://img.shields.io/npm/v/@opengis/billing)](https://www.npmjs.com/package/@opengis/billing)
4
+ [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
5
+
6
+ It standardizes the entire authorization process, while taking care of everything from login to logout
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm i @opengis/billing
12
+ ```
13
+ ## Publish
14
+
15
+ ```bash
16
+ npm run build
17
+ npm publish
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```js
23
+ fastify.register(import('@opengis/billing'), config);
24
+ ```
25
+
26
+ ## Documenation
27
+
28
+ For a detailed understanding of `billing`, its features, and how to use them, refer to our [Documentation](https://apidocs.softpro.ua/opengis/plugin/).
package/config.js ADDED
@@ -0,0 +1,7 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+
3
+ const config = existsSync('config.json') ? JSON.parse(readFileSync('config.json')) : {};
4
+ // console.log(config);
5
+ config.storageList = {};
6
+
7
+ export default config;
@@ -0,0 +1,80 @@
1
+ const q = `insert into billing.trx(refill_sum, account_id, uid, referer_url, trx_status)
2
+ select $1::numeric, $2, $3, $4, 'inprogress' returning trx_id`;
3
+ const q1 = 'select account_id from billing.account_user where client_uid=$1 limit 1';
4
+
5
+ import request from 'request-promise';
6
+
7
+ /**
8
+ * Апі перенаправлює до стороннього сервісу оплати карткою онлайн
9
+ *
10
+ * @method GET|POST
11
+ * @alias PortmonePaymentRedirect
12
+ * @summary redirect на сервіс оплати карткою
13
+ * @type api
14
+ * @param {String} price - Сума поповнення рахунку
15
+ * @returns {Number} status - номер помилки. Повертається, якщо була допущенна помилка в отриманих параметрах, або з бази. Це може бути 400 або 500
16
+ * @returns {String} error - опис помилки
17
+ * @returns {String} redirect - шлях до переадресації
18
+ * @returns {String} message - повідомлення про успішне виконання і передача певних даних або помилку
19
+ * @returns {String} headers - заголовки HTTP
20
+ * @returns {String} file - шлях файла або його назва
21
+ */
22
+
23
+ export default async function PortmoneRedirect(req, res) {
24
+ const {
25
+ pg, funcs, user = {}, query = {}, headers = {}, log,
26
+ } = req;
27
+
28
+ if (!user?.uid) {
29
+ return { message: 'uid is required', status: 403 };
30
+ }
31
+
32
+ if (!query?.price) {
33
+ return { message: 'price is required', status: 400 };
34
+ }
35
+
36
+ const {
37
+ host = 'https://www.portmone.com.ua',
38
+ payeeId = 1185, // test account payee id
39
+ } = funcs.config?.integration?.portmone || {};
40
+
41
+ const { account_id: accountId } = await pg.query(q1, [user.uid]).then((res1) => res1.rows?.[0] || {});
42
+
43
+ const args = [query?.price, accountId, user.uid, headers?.referer];
44
+ const { trx_id: trxId } = await pg.query(q, args).then((res1) => res1.rows?.[0] || {});
45
+
46
+ if (!trxId) {
47
+ return { message: 'Не вірне замовлення', status: 400 };
48
+ }
49
+
50
+ const domain = `${req.protocol}://${req.hostname}`;
51
+ const body = {
52
+ payee_id: payeeId,
53
+ description: `Послуга - поповнення рахунку на суму ${query?.price} грн`,
54
+ shop_order_number: trxId,
55
+ bill_refill_sum: query?.price,
56
+ success_url: `${domain}${funcs.config?.prefix || '/api'}/portmone/${trxId}/success`,
57
+ failure_url: `${domain}${funcs.config?.prefix || '/api'}/portmone/${trxId}/error`,
58
+ lang: 'ua',
59
+ };
60
+ try {
61
+ const data = await request({
62
+ url: `${host}/gateway/`,
63
+ method: 'POST',
64
+ formData: body,
65
+ followAllRedirects: false,
66
+ jar: true,
67
+ timeout: 6000,
68
+ });
69
+ log.info('integrations/portmone/redirect', {
70
+ payeeId, trxId, accountId, price: query?.price, domain, response: data, status: 302,
71
+ });
72
+ return res.redirect(data);
73
+ }
74
+ catch (err) {
75
+ log.info('integrations/portmone/redirect', {
76
+ payeeId, trxId, accountId, price: query?.price, domain, response: err.response.headers?.location, status: err.statusCode,
77
+ });
78
+ return res.redirect(err.response.headers?.location);
79
+ }
80
+ }
@@ -0,0 +1,113 @@
1
+ const template = 'marketplace-service-payment-client-template';
2
+ const q = 'select trx_id as trx, refill_sum, referer_url, trx_status, account_id from billing.trx where trx_id=$1';
3
+ const q1 = `select total_balance from billing.trx where account_id=$1
4
+ and total_balance is not null order by cdate desc limit 1`;
5
+ const q2 = 'select email from crm_acc.crm_account where account_id=$1';
6
+
7
+ import getSelectVal from '@opengis/fastify-table/table/funcs/metaFormat/getSelectVal.js';
8
+
9
+ /**
10
+ * Апі перенаправлює з portmone назад на сторінку замовлення
11
+ *
12
+ * @method POST
13
+ * @alias PortmoneStatus
14
+ * @summary redirect з portmone на сторінку замовлення
15
+ * @type api
16
+ * @param {String} trxId - Ідентифікатор замовлення
17
+ * @param {String} refillSum - Сума поповнення рахунку
18
+ * @returns {Number} status - номер помилки. Повертається, якщо була допущенна помилка в отриманих параметрах, або з бази. Це може бути 400 або 500
19
+ * @returns {String} error - опис помилки
20
+ * @returns {String} redirect - шлях до переадресації
21
+ * @returns {String} message - повідомлення про успішне виконання і передача певних даних або помилку
22
+ * @returns {String} headers - заголовки HTTP
23
+ * @returns {String} file - шлях файла або його назва
24
+ */
25
+
26
+ export default async function portmoneStatus(req, res) {
27
+ const {
28
+ body = {}, pg, log, params = {}, funcs = {}, headers = {},
29
+ } = req;
30
+
31
+ const {
32
+ host = 'https://www.portmone.com.ua',
33
+ } = funcs.config?.integration?.portmone || {};
34
+
35
+ const origin = headers?.origin || headers?.referer;
36
+ log.debug('integrations/portmone/status', { body, params, origin });
37
+
38
+ const responseStatus = body?.status || params?.status;
39
+ const trxId = body?.shopOrderNumber || params?.id;
40
+
41
+ const updateTrx = `
42
+ update billing.trx
43
+ set payment_data=$2, trx_status=$3
44
+ ${responseStatus === 'success'
45
+ ? ',transaction_success_date=now(),refill_sum=$4,total_balance=( $4::numeric + coalesce($5::numeric,0) ) '
46
+ : ''}
47
+ where trx_id=$1`;
48
+
49
+ try {
50
+ const {
51
+ trx, refill_sum: refillSum, referer_url: redirectUrl, trx_status: trxStatus, account_id: accountId,
52
+ } = await pg.query(q, [trxId]).then((res1) => res1.rows?.[0] || {});
53
+
54
+ if (!trx) {
55
+ return { message: 'Transaction not found', status: 400 };
56
+ }
57
+
58
+ const refill = body?.billAmount || refillSum || 0;
59
+
60
+ // request portmone trx info to recheck request data is valid?
61
+ if (!req.unittest && !origin?.startsWith(host) && false) {
62
+ return { message: 'access restricted', status: 403 };
63
+ }
64
+
65
+ // last trx contains total balance
66
+ const { total_balance: totalBalance } = await pg.query(q1, [accountId]).then((res1) => res1.rows?.[0] || {});
67
+
68
+ if (trxStatus === 'success') {
69
+ return { message: 'Transaction was already processed', status: 400 };
70
+ }
71
+
72
+ await pg.query(updateTrx, [trxId, JSON.stringify(body), responseStatus, refill, totalBalance]);
73
+
74
+ const { email } = await pg.query(q2, [accountId]).then((res1) => res1.rows?.[0] || {});
75
+
76
+ log.debug('integrations/portmone/status', {
77
+ email, redirect: redirectUrl, template, trxId,
78
+ });
79
+
80
+ const data = await pg.one('select * from billing.trx where trx_id=$1', [trxId]);
81
+
82
+ const cls1 = await getSelectVal({ name: 'billing.service_id', values: [data.service_id] });
83
+ const cls2 = await getSelectVal({ name: 'billing.account_id', values: [data.account_id] });
84
+ const cls3 = await getSelectVal({ name: 'billing.trx_status', values: [data.trx_status] });
85
+
86
+ Object.assign(data, {
87
+ domain: req.hostname,
88
+ service_id_text: data.service_id ? cls1?.[data.service_id] || data.service_id : 'Інформація відсутня',
89
+ account_id_text: data.account_id ? cls2?.[data.account_id] || data.account_id : 'Інформація відсутня',
90
+ trx_status_text: data.trx_status ? cls3?.[data.trx_status] || data.trx_status : 'Інформація відсутня',
91
+ });
92
+ if (email) {
93
+ const options = {
94
+ pg,
95
+ funcs,
96
+ log,
97
+ to: email,
98
+ title: `Поповнення рахунку на порталі ${req.hostname}`,
99
+ template,
100
+ data,
101
+ // table: 'billing.trx',
102
+ // id: trxId,
103
+ nocache: funcs.config?.local,
104
+ };
105
+ await funcs.notification(options);
106
+ }
107
+ return res.redirect(redirectUrl);
108
+ }
109
+ catch (err) {
110
+ log.debug('integrations/portmone/status', { trxId, responseStatus, error: err.toString() });
111
+ return { error: err.toString(), status: 500 };
112
+ }
113
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@opengis/pay",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "pay plugins for billing",
6
+ "main": "plugin.js",
7
+ "browser": "plugin.js",
8
+ "files": [
9
+ "controllers/*",
10
+ "funcs/*",
11
+ "server/*",
12
+ "plugin.js",
13
+ "config.js"
14
+ ],
15
+ "scripts": {
16
+ "test": "node --test",
17
+ "start": "node --watch-path=server server",
18
+ "dev": "fastify start -w -l info -P server/app.js",
19
+ "debug": "cross-env NODE_ENV=production node server --inspect-brk",
20
+ "prod": "cross-env NODE_ENV=production npm run start",
21
+ "release": "npm run build && npm publish",
22
+ "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
23
+ },
24
+ "dependencies": {
25
+ "@opengis/fastify-auth": "^1.0.22",
26
+ "@opengis/fastify-table": "^1.0.82",
27
+ "cross-env": "^7.0.3",
28
+ "fastify": "^4.26.1",
29
+ "fastify-plugin": "^4.0.0",
30
+ "request-promise": "^4.2.6"
31
+ },
32
+ "devDependencies": {
33
+ "@vue/eslint-config-prettier": "^9.0.0",
34
+ "@vue/eslint-config-typescript": "^12.0.0",
35
+ "eslint": "^8.49.0",
36
+ "eslint-config-airbnb": "^19.0.4",
37
+ "eslint-plugin-import": "^2.25.3",
38
+ "eslint-plugin-playwright": "^1.5.2",
39
+ "eslint-plugin-vue": "^9.17.0"
40
+ },
41
+ "author": "Softpro",
42
+ "license": "ISC"
43
+ }
package/plugin.js ADDED
@@ -0,0 +1,35 @@
1
+ import fp from 'fastify-plugin';
2
+
3
+ import portmoneRedirect from './controllers/portmone/portmone.redirect.js';
4
+ import portmoneStatus from './controllers/portmone/portmone.status.js';
5
+
6
+ import config from './config.js';
7
+
8
+ /**
9
+ * This plugins adds some utilities to handle http errors
10
+ *
11
+ * @see https://github.com/fastify/fastify-sensible
12
+ */
13
+
14
+ async function plugin(fastify, opt = {}) {
15
+ config.redis = opt.redis;
16
+ config.pg = opt.pg;
17
+ const prefix = opt?.prefix || config.prefix || '/api';
18
+
19
+ fastify.register(import('./server/plugins/hook.js'));
20
+
21
+ fastify.route({
22
+ method: 'GET',
23
+ path: `${prefix}/portmone-redirect`,
24
+ config: { policy: ['public'] },
25
+ handler: portmoneRedirect,
26
+ });
27
+ fastify.route({
28
+ method: 'POST',
29
+ path: `${prefix}/portmone/:id/:status`,
30
+ config: { policy: ['public'] },
31
+ handler: portmoneStatus,
32
+ });
33
+ }
34
+
35
+ export default fp(plugin);
@@ -0,0 +1,89 @@
1
+ create schema if not exists admin;
2
+
3
+ -- DROP TABLE is exists admin.users;
4
+ CREATE TABLE if not exists admin.users();
5
+ ALTER TABLE admin.users add column if not exists uid text NOT NULL DEFAULT next_id();
6
+ ALTER TABLE admin.users DROP CONSTRAINT if exists admin_user_uid_pkey cascade;
7
+ ALTER TABLE admin.users DROP CONSTRAINT if exists user_pk cascade;
8
+
9
+ ALTER TABLE admin.users add column if not exists login text;
10
+ ALTER TABLE admin.users add column if not exists password text NOT NULL DEFAULT ''::text;
11
+ ALTER TABLE admin.users add column if not exists user_name text;
12
+ ALTER TABLE admin.users add column if not exists sur_name text;
13
+ ALTER TABLE admin.users add column if not exists father_name text;
14
+ ALTER TABLE admin.users add column if not exists email text;
15
+ ALTER TABLE admin.users add column if not exists phone text;
16
+ ALTER TABLE admin.users add column if not exists avatar text;
17
+ ALTER TABLE admin.users add column if not exists enabled boolean;
18
+ ALTER TABLE admin.users add column if not exists user_personal_code text;
19
+ ALTER TABLE admin.users add column if not exists last_activity_date timestamp without time zone;
20
+ ALTER TABLE admin.users add column if not exists user_type text DEFAULT 'regular'::text;
21
+ ALTER TABLE admin.users add column if not exists salt text;
22
+ ALTER TABLE admin.users add column if not exists cdate timestamp without time zone DEFAULT date_trunc('seconds'::text, now());
23
+ ALTER TABLE admin.users add column if not exists editor_id text;
24
+ ALTER TABLE admin.users add column if not exists editor_date timestamp without time zone;
25
+
26
+ ALTER TABLE admin.users add CONSTRAINT admin_user_uid_pkey PRIMARY KEY (uid);
27
+
28
+ COMMENT ON TABLE admin.users IS 'Користувачі';
29
+
30
+ COMMENT ON COLUMN admin.users.uid IS 'ID користувача';
31
+ COMMENT ON COLUMN admin.users.login IS 'Логін користувача';
32
+ COMMENT ON COLUMN admin.users.password IS 'Пароль користувача';
33
+ COMMENT ON COLUMN admin.users.user_name IS 'Ім''я користувача';
34
+ COMMENT ON COLUMN admin.users.sur_name IS 'Прізвище користувача';
35
+ COMMENT ON COLUMN admin.users.father_name IS 'По-батькові користувача';
36
+ COMMENT ON COLUMN admin.users.email IS 'Ел. пошта користувача';
37
+ COMMENT ON COLUMN admin.users.phone IS 'Номер телефону користувача';
38
+ COMMENT ON COLUMN admin.users.avatar IS 'Аватар';
39
+ COMMENT ON COLUMN admin.users.enabled IS 'On / Off';
40
+ COMMENT ON COLUMN admin.users.last_activity_date IS 'Дата останньої активності';
41
+ COMMENT ON COLUMN admin.users.user_type IS 'Тип користувача';
42
+ COMMENT ON COLUMN admin.users.salt IS 'Сіль';
43
+
44
+ CREATE EXTENSION if not exists pgcrypto SCHEMA public VERSION "1.3";
45
+ CREATE OR REPLACE FUNCTION admin.crypt(text, text) RETURNS text AS '$libdir/pgcrypto', 'pg_crypt' LANGUAGE c IMMUTABLE STRICT COST 1;
46
+
47
+ -- DROP FUNCTION admin.insert_update_user_before();
48
+ CREATE OR REPLACE FUNCTION admin.insert_update_user_before()
49
+ RETURNS trigger AS
50
+
51
+ $BODY$
52
+ DECLARE
53
+
54
+ iterations int;
55
+ hash character varying;
56
+
57
+ BEGIN
58
+
59
+ if(TG_OP='INSERT' or (TG_OP='UPDATE' and new.password<>old.password)) then
60
+ if(char_length(new.password) <> 0 and char_length(new.password) < 8) then
61
+ --raise exception 'password must be longer than 8 characters';
62
+ end if;
63
+ new.salt=md5(now()::text);
64
+ --raise exception '%','change pass';
65
+ if(new.salt ='') then
66
+ new.salt=gen_salt('md5');
67
+ end if;
68
+ iterations = 10;
69
+ hash='';
70
+
71
+ loop
72
+ if iterations=0 then
73
+ exit;
74
+ end if;
75
+ hash = md5(new.password||hash||new.salt);
76
+ iterations=iterations-1;
77
+ end loop;
78
+ new.password=admin.crypt(hash,new.salt);
79
+
80
+ end if;
81
+ RETURN new;
82
+ END
83
+ $BODY$
84
+
85
+ LANGUAGE plpgsql VOLATILE COST 100;
86
+
87
+ DROP TRIGGER if exists insert_update_user_before on admin.users;
88
+ CREATE TRIGGER insert_update_user_before BEFORE INSERT OR UPDATE ON admin.users FOR EACH ROW
89
+ EXECUTE PROCEDURE admin.insert_update_user_before();
@@ -0,0 +1,16 @@
1
+ import fp from 'fastify-plugin';
2
+
3
+ // import pgClients from '@opengis/fastify-table/pg/pgClients.js';
4
+ // import getTemplate from '@opengis/fastify-table/table/controllers/utils/getTemplate.js';
5
+
6
+ async function plugin(fastify) {
7
+ fastify.addHook('onListen', async () => {
8
+ if (fastify.execMigrations) {
9
+ await fastify.execMigrations();
10
+ }
11
+ });
12
+
13
+ // fastify.addHook('onListen', async () => {});
14
+ }
15
+
16
+ export default fp(plugin);
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "id": "inprogress",
4
+ "text": "Не оплачено"
5
+ },
6
+ {
7
+ "id": "success",
8
+ "text": "Оплачено"
9
+ },
10
+ {
11
+ "id": "error",
12
+ "text": "Помилка при оплаті"
13
+ }
14
+ ]
@@ -0,0 +1,149 @@
1
+ <div>
2
+ <br>
3
+ </div>
4
+ <div>
5
+ <table style="width:100%;">
6
+ <tbody>
7
+ <tr>
8
+ <td style="width:20px;">&nbsp;</td>
9
+ <td align="center">
10
+ <table style="width:100%" bgcolor="#ffffff">
11
+ <tbody>
12
+ <tr>
13
+ <td align="center"><a href="//{{domain}}" class="logo"><img src="https://softpro.ua/tpl/img/logo.svg"/></a>
14
+ </td>
15
+ </tr>
16
+
17
+ <tr>
18
+ <td style="padding:7px 0" align="center"><span style="color:#555454;font-family:'Open-sans',sans-serif;font-size:small">
19
+ <span style="font-weight:500;font-size:16px;text-transform:uppercase;line-height:25px">{{account_id_text}}, дякуємо за здійснення замовлення на порталі <a href="//{{domain}}">{{domain}}</a></span> </span>
20
+ </td>
21
+ </tr>
22
+ <tr>
23
+ <td style="padding:0">&nbsp;</td>
24
+ </tr>
25
+ <tr>
26
+ <td style="box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);padding:7px 0;border-radius:30px">
27
+ <table style="width:100%">
28
+ <tbody>
29
+ <tr >
30
+ <td style="padding:7px 0" width="10">&nbsp;</td>
31
+ <td style="padding:7px 0">
32
+ <span style="color:#555454;font-family:'Open-sans',sans-serif;font-size:small"> </span>
33
+ {{#if service_id}}
34
+ <p style="color:#61677b;border-bottom:1px solid #61677b;margin:3px 0 7px;text-transform:uppercase;font-weight:500;font-size:18px;padding-bottom:10px">Послуга: {{service_id_text}}</p>
35
+ {{/if}}
36
+ <br>
37
+ <span style="color:#61677b"><strong>Дата замовлення:</strong> {{formatDate cdate format='dd.mm.yy hh:mi'}}</span>
38
+ <br>
39
+ <span style="color:#61677b"><strong>Статус замовлення:</strong> {{trx_status_text}}</span>
40
+ {{#if withdrawal_sum}}
41
+ <br>
42
+ <span style="color:#61677b"><strong>Вартість послуги:</strong> {{num_format withdrawal_sum dec="2"}}</span>
43
+ {{/if}}
44
+ {{#if refill_sum}}
45
+ <br>
46
+ <span style="color:#61677b"><strong>Поповнення рахунку:</strong> {{num_format refill_sum dec="2"}}</span>
47
+ {{/if}}
48
+ {{#if total_balance}}
49
+ <br>
50
+ <span style="color:#61677b"><strong>Поточний баланс:</strong> {{num_format total_balance dec="2"}}</span>
51
+ {{/if}}
52
+ </td>
53
+ <td style="padding:7px 0" width="10">&nbsp;</td>
54
+ </tr>
55
+ </tbody>
56
+ </table>
57
+ </td>
58
+ </tr>
59
+
60
+ <tr>
61
+ <td style="padding:7px 0">
62
+ <span style="color:#555454;font-family:'Open-sans',sans-serif;font-size:small"> </span>
63
+ <table style="width:100%;border-collapse:collapse" bgcolor="#ffffff">
64
+ <tbody>
65
+ <tr>
66
+ <th style="border:1px solid #d6d4d4;background-color:#fbfbfb;color:#61677b;font-family:Arial;font-size:13px;padding:10px" bgcolor="#f8f8f8">Назва послуги</th>
67
+ <th style="border:1px solid #d6d4d4;background-color:#fbfbfb;color:#61677b;font-family:Arial;font-size:13px;padding:10px" bgcolor="#f8f8f8" width="17%">Ціна послуги</th>
68
+ </tr>
69
+ <tr>
70
+ <td style="border:1px solid #d6d4d4">
71
+ <table>
72
+ <tbody>
73
+ <tr>
74
+ <td style="padding: 0 10px">
75
+ <font size="2" face="Open-sans, sans-serif" color="#555454">
76
+ <strong>
77
+ {{#if service_id}}
78
+ {{service_id_text}}
79
+ {{^}}
80
+ {{#if refill_sum}}
81
+ Поповнення рахунку
82
+ {{/if}}
83
+ {{/if}}
84
+ </strong>
85
+ </font>
86
+ </td>
87
+ </tr>
88
+ </tbody>
89
+ </table>
90
+ </td>
91
+ <td style="border:1px solid #d6d4d4">
92
+ <table>
93
+ <tbody>
94
+ <tr>
95
+ <td width="10">&nbsp;</td>
96
+ <td align="right">
97
+ <font size="2" face="Open-sans, sans-serif" color="#555454"> {{num_format (coalesce withdrawal_sum refill_sum)}} грн.</font>
98
+ </td>
99
+ <td width="10">&nbsp;</td>
100
+ </tr>
101
+ </tbody>
102
+ </table>
103
+ </td>
104
+ </tr>
105
+
106
+ <tr>
107
+ <td colspan="2" style="border:1px solid #d6d4d4;text-align:center;color:#777;padding:7px 0">&nbsp;&nbsp;</td>
108
+ </tr>
109
+
110
+ <tr>
111
+ <td colspan="1" style="border:1px solid #d6d4d4;color:#61677b;padding:7px 0" bgcolor="#f8f8f8">
112
+ <table style="width:100%;border-collapse:collapse">
113
+ <tbody>
114
+ <tr>
115
+ <td style="color:#61677b;padding:0" width="10">&nbsp;</td>
116
+ <td style="color:#61677b;padding:0" align="right"><span style="color:#555454;font-family:'Open-sans',sans-serif;font-size:small"> <strong>Поточний баланс</strong> </span></td>
117
+ <td style="color:#61677b;padding:0" width="10">&nbsp;</td>
118
+ </tr>
119
+ </tbody>
120
+ </table>
121
+ </td>
122
+ <td colspan="1" style="border:1px solid #d6d4d4;color:#61677b;padding:7px 0" bgcolor="#f8f8f8">
123
+ <table style="width:100%;border-collapse:collapse">
124
+ <tbody>
125
+ <tr>
126
+ <td style="color:#61677b;padding:0" width="10">&nbsp;</td>
127
+ <td style="color:#61677b;padding:0" align="right"><span style="color:#555454;font-family:'Open-sans',sans-serif;font-size:large">{{total_balance}} грн. </span></td>
128
+ <td style="color:#61677b;padding:0" width="10">&nbsp;</td>
129
+ </tr>
130
+ </tbody>
131
+ </table>
132
+ </td>
133
+ </tr>
134
+ </tbody>
135
+ </table>
136
+ </td>
137
+ </tr>
138
+
139
+ <tr>
140
+ <td style="border-top:4px solid #61677b;padding:7px 0"><span><a href="//{{domain}}"><img src="https://softpro.ua/tpl/img/logo.svg"/></a></span></td>
141
+ </tr>
142
+ </tbody>
143
+ </table>
144
+ </td>
145
+ <td style="width:20px;">&nbsp;</td>
146
+ </tr>
147
+ </tbody>
148
+ </table>
149
+ </div>
@@ -0,0 +1,5 @@
1
+ select account_id, account_name from billing.account b
2
+ left join lateral (
3
+ select account_name from crm_acc.crm_account where account_id=b.account_id limit 1
4
+ )c on 1=1
5
+ order by account_name
@@ -0,0 +1 @@
1
+ select service_id, service_name from billing.service order by service_name