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/package.json CHANGED
@@ -1,62 +1,62 @@
1
- {
2
- "name": "piper-utils",
3
- "version": "1.1.69",
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
- * Scope a WHERE clause to the partner's own business (partnerBusinessId).
7
- * Use for "mine" resources: deal, activity, application, template.
8
- *
9
- * global → deletes where.businessId (super sees everything)
10
- * partner → sets where.businessId = partnerBusinessId
11
- * throws partnerNotConfigured if partnerBusinessId is missing
12
- * standard no-op (createFilters already handled)
13
- *
14
- * @param {object} where - Sequelize WHERE clause to mutate.
15
- * @param {object} access - Access object from resolveAccess.
16
- * @param {{ getPartnerById: Function }} deps
17
- */
18
- export async function scopeToOwnBusiness(where, access, { getPartnerById } = {}) {
19
- if (!access) {
20
- throw errorList.unauthorized;
21
- }
22
- if (access.level === 'global') {
23
- delete where.businessId;
24
- return;
25
- }
26
- if (access.level === 'partner') {
27
- await ensurePartnerScope(access, { getPartnerById });
28
- if (!access.partnerBusinessId) {
29
- throw errorList.partnerNotConfigured;
30
- }
31
- where.businessId = access.partnerBusinessId;
32
- return;
33
- }
34
- // standard → no-op (createFilters already injected from JWT)
35
- }
36
-
37
- /**
38
- * Scope a WHERE clause to the partner's portfolio (businessIds array).
39
- * Use for "book" resources (future transaction lists, merchant inbox views).
40
- *
41
- * global → deletes where.businessId
42
- * partner → sets where.businessId = businessIds[]
43
- * standard → no-op
44
- *
45
- * @param {object} where - Sequelize WHERE clause to mutate.
46
- * @param {object} access - Access object from resolveAccess.
47
- * @param {{ getPartnerById: Function }} deps
48
- */
49
- export async function scopeToPartnerBook(where, access, { getPartnerById } = {}) {
50
- if (!access) {
51
- throw errorList.unauthorized;
52
- }
53
- if (access.level === 'global') {
54
- delete where.businessId;
55
- return;
56
- }
57
- if (access.level === 'partner') {
58
- await ensurePartnerScope(access, { getPartnerById });
59
- where.businessId = access.businessIds || [];
60
- return;
61
- }
62
- // standard no-op
63
- }
64
-
65
- /**
66
- * Scope to the union of partnerBusinessId + businessIds.
67
- * Use for tickets — partner sees own tickets + portfolio merchants' tickets.
68
- *
69
- * global deletes where.businessId
70
- * partner → sets where.businessId = [partnerBusinessId, ...businessIds]
71
- * standard → sets where.businessId = access.businessIds
72
- * (explicitly handled here because ticket routes may not call
73
- * createFilters before scoping, e.g. delete/resolve handlers)
74
- *
75
- * @param {object} where - Sequelize WHERE clause to mutate.
76
- * @param {object} access - Access object from resolveAccess.
77
- * @param {{ getPartnerById: Function }} deps
78
- */
79
- export async function scopeToBookUnionOwn(where, access, { getPartnerById } = {}) {
80
- if (!access) {
81
- throw errorList.unauthorized;
82
- }
83
- if (access.level === 'global') {
84
- delete where.businessId;
85
- return;
86
- }
87
- if (access.level === 'partner') {
88
- await ensurePartnerScope(access, { getPartnerById });
89
- const ids = _.uniq(
90
- [access.partnerBusinessId, ...(access.businessIds || [])].filter(Boolean)
91
- );
92
- where.businessId = ids;
93
- return;
94
- }
95
- if (access.level === 'standard') {
96
- where.businessId = access.businessIds;
97
- return;
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
+ }