piper-utils 1.1.69 → 1.1.71
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/bin/main.js +79 -20
- package/bin/main.js.map +1 -1
- package/package.json +62 -62
- package/src/contract/contract.js +42 -0
- package/src/contract/contract.test.js +36 -0
- package/src/database/dbUtils/partnerAccess/accessScope.js +128 -99
- package/src/database/dbUtils/partnerAccess/accessScope.test.js +287 -287
- package/src/database/dbUtils/partnerAccess/createAccessHelpers.js +3 -3
- package/src/database/dbUtils/queryStringUtils/createFilters.js +311 -311
- package/src/database/dbUtils/queryStringUtils/createFilters.test.js +682 -682
- package/src/eventManager/handleFile.js +129 -115
- package/src/eventManager/handleFile.test.js +463 -460
- package/src/index.js +4 -0
- package/src/requestResponse/requestResponse.test.js +58 -0
package/package.json
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "piper-utils",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "Utility library for Piper",
|
|
5
|
-
"main": "bin/main.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "cross-env BUILD_ENV='test' nyc jasmine JASMINE_CONFIG_PATH=jasmine.json",
|
|
8
|
-
"itest": "cross-env BUILD_ENV='development' nyc jasmine JASMINE_CONFIG_PATH=jasmine.json",
|
|
9
|
-
"build": "webpack --config webpack.config.js",
|
|
10
|
-
"build-prod": "cross-env BUILD_ENV='production' webpack --config webpack.config.js"
|
|
11
|
-
},
|
|
12
|
-
"repository": {
|
|
13
|
-
"type": "git"
|
|
14
|
-
},
|
|
15
|
-
"author": "Kevin Batchelor",
|
|
16
|
-
"license": "PVT",
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"@babel/cli": "7.11.6",
|
|
19
|
-
"@babel/core": "7.23.2",
|
|
20
|
-
"@babel/preset-env": "^7.29.0",
|
|
21
|
-
"@babel/register": "^7.28.6",
|
|
22
|
-
"@babel/runtime": "8.0.0-alpha.17",
|
|
23
|
-
"aws-sdk-client-mock": "^3.0.1",
|
|
24
|
-
"babel-loader": "9.1.3",
|
|
25
|
-
"babel-preset-env": "1.7.0",
|
|
26
|
-
"cross-env": "7.0.3",
|
|
27
|
-
"jasmine": "5.1.0",
|
|
28
|
-
"nyc": "14.1.1",
|
|
29
|
-
"terser-webpack-plugin": "^5.3.10",
|
|
30
|
-
"webpack": "5.104.1",
|
|
31
|
-
"webpack-cli": "^5.1.4",
|
|
32
|
-
"webpack-node-externals": "3.0.0"
|
|
33
|
-
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"@aws-sdk/client-dynamodb": "^3.507.0",
|
|
36
|
-
"@aws-sdk/client-s3": "^3.507.0",
|
|
37
|
-
"@aws-sdk/client-sns": "^3.507.0",
|
|
38
|
-
"@aws-sdk/lib-dynamodb": "^3.507.0",
|
|
39
|
-
"babel-runtime": "6.26.0"
|
|
40
|
-
},
|
|
41
|
-
"peerDependencies": {
|
|
42
|
-
"bluebird": ">=3.7.0",
|
|
43
|
-
"dayjs": "^1.11.13",
|
|
44
|
-
"lodash": ">=4.17.15",
|
|
45
|
-
"sequelize": ">=6.6.2",
|
|
46
|
-
"umzug": ">=3.2.1"
|
|
47
|
-
},
|
|
48
|
-
"nyc": {
|
|
49
|
-
"require": [
|
|
50
|
-
"@babel/register"
|
|
51
|
-
],
|
|
52
|
-
"reporter": [
|
|
53
|
-
"lcov",
|
|
54
|
-
"text-summary"
|
|
55
|
-
],
|
|
56
|
-
"check-coverage": true,
|
|
57
|
-
"branches": 70,
|
|
58
|
-
"lines": 80,
|
|
59
|
-
"functions": 70,
|
|
60
|
-
"statements": 85
|
|
61
|
-
}
|
|
62
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "piper-utils",
|
|
3
|
+
"version": "1.1.71",
|
|
4
|
+
"description": "Utility library for Piper",
|
|
5
|
+
"main": "bin/main.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "cross-env BUILD_ENV='test' nyc jasmine JASMINE_CONFIG_PATH=jasmine.json",
|
|
8
|
+
"itest": "cross-env BUILD_ENV='development' nyc jasmine JASMINE_CONFIG_PATH=jasmine.json",
|
|
9
|
+
"build": "webpack --config webpack.config.js",
|
|
10
|
+
"build-prod": "cross-env BUILD_ENV='production' webpack --config webpack.config.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git"
|
|
14
|
+
},
|
|
15
|
+
"author": "Kevin Batchelor",
|
|
16
|
+
"license": "PVT",
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@babel/cli": "7.11.6",
|
|
19
|
+
"@babel/core": "7.23.2",
|
|
20
|
+
"@babel/preset-env": "^7.29.0",
|
|
21
|
+
"@babel/register": "^7.28.6",
|
|
22
|
+
"@babel/runtime": "8.0.0-alpha.17",
|
|
23
|
+
"aws-sdk-client-mock": "^3.0.1",
|
|
24
|
+
"babel-loader": "9.1.3",
|
|
25
|
+
"babel-preset-env": "1.7.0",
|
|
26
|
+
"cross-env": "7.0.3",
|
|
27
|
+
"jasmine": "5.1.0",
|
|
28
|
+
"nyc": "14.1.1",
|
|
29
|
+
"terser-webpack-plugin": "^5.3.10",
|
|
30
|
+
"webpack": "5.104.1",
|
|
31
|
+
"webpack-cli": "^5.1.4",
|
|
32
|
+
"webpack-node-externals": "3.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@aws-sdk/client-dynamodb": "^3.507.0",
|
|
36
|
+
"@aws-sdk/client-s3": "^3.507.0",
|
|
37
|
+
"@aws-sdk/client-sns": "^3.507.0",
|
|
38
|
+
"@aws-sdk/lib-dynamodb": "^3.507.0",
|
|
39
|
+
"babel-runtime": "6.26.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"bluebird": ">=3.7.0",
|
|
43
|
+
"dayjs": "^1.11.13",
|
|
44
|
+
"lodash": ">=4.17.15",
|
|
45
|
+
"sequelize": ">=6.6.2",
|
|
46
|
+
"umzug": ">=3.2.1"
|
|
47
|
+
},
|
|
48
|
+
"nyc": {
|
|
49
|
+
"require": [
|
|
50
|
+
"@babel/register"
|
|
51
|
+
],
|
|
52
|
+
"reporter": [
|
|
53
|
+
"lcov",
|
|
54
|
+
"text-summary"
|
|
55
|
+
],
|
|
56
|
+
"check-coverage": true,
|
|
57
|
+
"branches": 70,
|
|
58
|
+
"lines": 80,
|
|
59
|
+
"functions": 70,
|
|
60
|
+
"statements": 85
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Financing contract constants shared by the ERP (piper) and the payment
|
|
3
|
+
* gateway (piper-gateway). Both sides put these strings on the wire — the ERP
|
|
4
|
+
* writes them onto the order and the prepare payload, the gateway reads them
|
|
5
|
+
* back off the checkout page and echoes them home on the webhook — so they must
|
|
6
|
+
* agree exactly. Keeping them here is what stops a 'down' / 'Down' drift from
|
|
7
|
+
* becoming a silent mischarge.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* What the customer chose on the signing page.
|
|
12
|
+
* - full: pay the whole order total now; no financing, so no contract to sign
|
|
13
|
+
* - down: pay the down payment now and sign the financing contract for the rest
|
|
14
|
+
*/
|
|
15
|
+
export const contractChoices = {
|
|
16
|
+
full: 'full',
|
|
17
|
+
down: 'down'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Lifecycle of the contract attached to an order.
|
|
22
|
+
* - draft: order is still an estimate; nothing rendered yet
|
|
23
|
+
* - sent: contract body frozen and hashed, link is out with the customer
|
|
24
|
+
* - signed: customer consented and signed; signature + audit trail persisted
|
|
25
|
+
*/
|
|
26
|
+
export const contractStatuses = {
|
|
27
|
+
draft: 'Draft',
|
|
28
|
+
sent: 'Sent',
|
|
29
|
+
signed: 'Signed'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* True when value is a choice the signing page is allowed to send. The gateway
|
|
34
|
+
* gates on this before selecting which of the two server-side amounts to charge,
|
|
35
|
+
* so an unrecognized choice is a 400 rather than a wrong amount.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} value
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
export function isContractChoice(value) {
|
|
41
|
+
return Object.values(contractChoices).includes(value);
|
|
42
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { contractChoices, contractStatuses, isContractChoice } from './contract.js';
|
|
2
|
+
|
|
3
|
+
describe('contract constants', () => {
|
|
4
|
+
it('should expose the two payment choices the signing page can send', () => {
|
|
5
|
+
expect(contractChoices.full).toBe('full');
|
|
6
|
+
expect(contractChoices.down).toBe('down');
|
|
7
|
+
expect(Object.keys(contractChoices).length).toBe(2);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should expose the contract lifecycle statuses', () => {
|
|
11
|
+
expect(contractStatuses.draft).toBe('Draft');
|
|
12
|
+
expect(contractStatuses.sent).toBe('Sent');
|
|
13
|
+
expect(contractStatuses.signed).toBe('Signed');
|
|
14
|
+
expect(Object.keys(contractStatuses).length).toBe(3);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('isContractChoice', () => {
|
|
19
|
+
it('should accept every known choice', () => {
|
|
20
|
+
expect(isContractChoice(contractChoices.full)).toBe(true);
|
|
21
|
+
expect(isContractChoice(contractChoices.down)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should reject a case-mismatched choice so a drift becomes a 400 not a mischarge', () => {
|
|
25
|
+
expect(isContractChoice('Down')).toBe(false);
|
|
26
|
+
expect(isContractChoice('FULL')).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should reject unknown, empty and non-string values', () => {
|
|
30
|
+
expect(isContractChoice('partial')).toBe(false);
|
|
31
|
+
expect(isContractChoice('')).toBe(false);
|
|
32
|
+
expect(isContractChoice(null)).toBe(false);
|
|
33
|
+
expect(isContractChoice(undefined)).toBe(false);
|
|
34
|
+
expect(isContractChoice(0)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,99 +1,128 @@
|
|
|
1
|
-
import _ from 'lodash';
|
|
2
|
-
import { ensurePartnerScope } from './accessContext.js';
|
|
3
|
-
import { errorList } from '../../../requestResponse/errorCodes.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { ensurePartnerScope } from './accessContext.js';
|
|
3
|
+
import { errorList } from '../../../requestResponse/errorCodes.js';
|
|
4
|
+
|
|
5
|
+
// `requested` is the caller-supplied businessIds array (what the route extracts via
|
|
6
|
+
// getRequestedBusinessIds(event)). Empty array = no explicit filter (back-compat path).
|
|
7
|
+
// When non-empty it is intersected against the role's allowed set so a user can never
|
|
8
|
+
// widen their scope past what their role permits by passing a query-string businessId.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Scope a WHERE clause to the partner's own business (partnerBusinessId).
|
|
12
|
+
* Use for "mine" resources: deal, activity, application, template.
|
|
13
|
+
*
|
|
14
|
+
* global → requested ? where.businessId = requested : deletes where.businessId
|
|
15
|
+
* partner → requested ? [partnerBusinessId] ∩ requested : where.businessId = partnerBusinessId
|
|
16
|
+
* throws partnerNotConfigured if partnerBusinessId is missing
|
|
17
|
+
* standard → no-op (createFilters already handled)
|
|
18
|
+
*
|
|
19
|
+
* @param {object} where - Sequelize WHERE clause to mutate.
|
|
20
|
+
* @param {object} access - Access object from resolveAccess.
|
|
21
|
+
* @param {string[]} [requested] - Explicit businessIds from the query string.
|
|
22
|
+
* @param {{ getPartnerById: Function }} deps
|
|
23
|
+
*/
|
|
24
|
+
export async function scopeToOwnBusiness(where, access, requested = [], { getPartnerById } = {}) {
|
|
25
|
+
if (!access) {
|
|
26
|
+
throw errorList.unauthorized;
|
|
27
|
+
}
|
|
28
|
+
if (access.level === 'global') {
|
|
29
|
+
if (requested.length) {
|
|
30
|
+
where.businessId = requested;
|
|
31
|
+
} else {
|
|
32
|
+
delete where.businessId;
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (access.level === 'partner') {
|
|
37
|
+
await ensurePartnerScope(access, { getPartnerById });
|
|
38
|
+
if (!access.partnerBusinessId) {
|
|
39
|
+
throw errorList.partnerNotConfigured;
|
|
40
|
+
}
|
|
41
|
+
if (requested.length) {
|
|
42
|
+
where.businessId = _.intersection([access.partnerBusinessId], requested);
|
|
43
|
+
} else {
|
|
44
|
+
where.businessId = access.partnerBusinessId;
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// standard → no-op (createFilters already injected from JWT)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Scope a WHERE clause to the partner's portfolio (businessIds array).
|
|
53
|
+
* Use for "book" resources (future transaction lists, merchant inbox views).
|
|
54
|
+
*
|
|
55
|
+
* global → requested ? where.businessId = requested : deletes where.businessId
|
|
56
|
+
* partner → requested ? businessIds ∩ requested : where.businessId = businessIds[]
|
|
57
|
+
* (own business is intentionally excluded — book is portfolio only)
|
|
58
|
+
* standard → no-op
|
|
59
|
+
*
|
|
60
|
+
* @param {object} where - Sequelize WHERE clause to mutate.
|
|
61
|
+
* @param {object} access - Access object from resolveAccess.
|
|
62
|
+
* @param {string[]} [requested] - Explicit businessIds from the query string.
|
|
63
|
+
* @param {{ getPartnerById: Function }} deps
|
|
64
|
+
*/
|
|
65
|
+
export async function scopeToPartnerBook(where, access, requested = [], { getPartnerById } = {}) {
|
|
66
|
+
if (!access) {
|
|
67
|
+
throw errorList.unauthorized;
|
|
68
|
+
}
|
|
69
|
+
if (access.level === 'global') {
|
|
70
|
+
if (requested.length) {
|
|
71
|
+
where.businessId = requested;
|
|
72
|
+
} else {
|
|
73
|
+
delete where.businessId;
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (access.level === 'partner') {
|
|
78
|
+
await ensurePartnerScope(access, { getPartnerById });
|
|
79
|
+
const allowed = access.businessIds || [];
|
|
80
|
+
where.businessId = requested.length ? _.intersection(allowed, requested) : allowed;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// standard → no-op
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Scope to the union of partnerBusinessId + businessIds.
|
|
88
|
+
* Use for tickets — partner sees own tickets + portfolio merchants' tickets.
|
|
89
|
+
*
|
|
90
|
+
* global → requested ? where.businessId = requested : deletes where.businessId
|
|
91
|
+
* partner → allowed = uniq([partnerBusinessId, ...businessIds]);
|
|
92
|
+
* requested ? allowed ∩ requested : where.businessId = allowed
|
|
93
|
+
* standard → allowed = businessIds;
|
|
94
|
+
* requested ? allowed ∩ requested : where.businessId = allowed
|
|
95
|
+
* (explicitly handled here because ticket routes may not call
|
|
96
|
+
* createFilters before scoping, e.g. delete/resolve handlers)
|
|
97
|
+
*
|
|
98
|
+
* @param {object} where - Sequelize WHERE clause to mutate.
|
|
99
|
+
* @param {object} access - Access object from resolveAccess.
|
|
100
|
+
* @param {string[]} [requested] - Explicit businessIds from the query string.
|
|
101
|
+
* @param {{ getPartnerById: Function }} deps
|
|
102
|
+
*/
|
|
103
|
+
export async function scopeToBookUnionOwn(where, access, requested = [], { getPartnerById } = {}) {
|
|
104
|
+
if (!access) {
|
|
105
|
+
throw errorList.unauthorized;
|
|
106
|
+
}
|
|
107
|
+
if (access.level === 'global') {
|
|
108
|
+
if (requested.length) {
|
|
109
|
+
where.businessId = requested;
|
|
110
|
+
} else {
|
|
111
|
+
delete where.businessId;
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (access.level === 'partner') {
|
|
116
|
+
await ensurePartnerScope(access, { getPartnerById });
|
|
117
|
+
const allowed = _.uniq(
|
|
118
|
+
[access.partnerBusinessId, ...(access.businessIds || [])].filter(Boolean)
|
|
119
|
+
);
|
|
120
|
+
where.businessId = requested.length ? _.intersection(allowed, requested) : allowed;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (access.level === 'standard') {
|
|
124
|
+
const allowed = access.businessIds || [];
|
|
125
|
+
where.businessId = requested.length ? _.intersection(allowed, requested) : allowed;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|