merchi_sdk_js 0.6.1 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "merchi_sdk_js",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "src/merchi.js",
6
6
  "module": "src/merchi.js",
@@ -8,6 +8,9 @@
8
8
  "files": [
9
9
  "src/"
10
10
  ],
11
+ "scripts": {
12
+ "test": "node --test"
13
+ },
11
14
  "dependencies": {
12
15
  "axios": "^0.27.2",
13
16
  "browser-or-node": "^2.1.1",
package/src/job.js CHANGED
@@ -30,6 +30,7 @@ import { VariationsGroup } from './variations_group.js';
30
30
  import { Item } from './item.js';
31
31
  import { JobComment } from './job_comment.js';
32
32
  import { JobNote } from './job_note.js';
33
+ import { JobOperationLog } from './job_operation_log.js';
33
34
  import { Reminder } from './reminder.js';
34
35
  import { InternalTag } from './internal_tag.js';
35
36
 
@@ -43,6 +44,7 @@ export function Job() {
43
44
  addPropertyTo(this, 'currency');
44
45
  addPropertyTo(this, 'quantity');
45
46
  addPropertyTo(this, 'jobNotes', JobNote);
47
+ addPropertyTo(this, 'operationLogs', JobOperationLog);
46
48
  addPropertyTo(this, 'jobType');
47
49
  addPropertyTo(this, 'product', Product);
48
50
  addPropertyTo(this, 'supplyChainRequestProduct', Product);
@@ -0,0 +1,65 @@
1
+ import { generateUUID } from './uuid.js';
2
+ import {
3
+ addPropertyTo,
4
+ fromJson,
5
+ getOne,
6
+ getList,
7
+ fromJsonList,
8
+ deleteOne,
9
+ } from './model.js';
10
+ import { Job } from './job.js';
11
+ import { User } from './user.js';
12
+
13
+ export function JobOperationLog() {
14
+ this.resource = '/job_operation_logs';
15
+ this.json = 'jobOperationLog';
16
+ this.temporaryId = generateUUID();
17
+
18
+ addPropertyTo(this, 'id');
19
+ addPropertyTo(this, 'job', Job);
20
+ addPropertyTo(this, 'user', User);
21
+ addPropertyTo(this, 'sourceType');
22
+ addPropertyTo(this, 'aiInvolved');
23
+ addPropertyTo(this, 'action');
24
+ addPropertyTo(this, 'payloadJson');
25
+ addPropertyTo(this, 'changesJson');
26
+ addPropertyTo(this, 'operationJson');
27
+ addPropertyTo(this, 'createdAt');
28
+
29
+ this.get = function (success, error, embed) {
30
+ var self = this;
31
+ function handleResponse(result) {
32
+ success(
33
+ fromJson(self, result[self.json], { makesDirty: false })
34
+ );
35
+ }
36
+ getOne({
37
+ resource: this.resource,
38
+ id: this.id(),
39
+ success: handleResponse,
40
+ error: error,
41
+ embed: embed,
42
+ });
43
+ };
44
+
45
+ this.delete = function (options) {
46
+ options = options || {};
47
+ var success = options.success || function () {},
48
+ error = options.error || function () {};
49
+ deleteOne(this.resource + '/' + this.id(), success, error);
50
+ };
51
+ }
52
+
53
+ export function JobOperationLogs() {
54
+ this.resource = '/job_operation_logs';
55
+ this.json = 'jobOperationLogs';
56
+ this.single = JobOperationLog;
57
+
58
+ this.get = function (success, error, parameters) {
59
+ var self = this;
60
+ function handleResponse(result) {
61
+ success(fromJsonList(self, result, { makesDirty: false }));
62
+ }
63
+ getList(this.resource, handleResponse, error, parameters || {});
64
+ };
65
+ }
package/src/merchi.js CHANGED
@@ -54,6 +54,7 @@ import { Invoice, Invoices } from './invoice.js';
54
54
  import { Job, Jobs } from './job.js';
55
55
  import { JobComment } from './job_comment.js';
56
56
  import { JobNote } from './job_note.js';
57
+ import { JobOperationLog, JobOperationLogs } from './job_operation_log.js';
57
58
  import { MerchiFile, MerchiFiles } from './merchi_file.js';
58
59
  import { Menu } from './menu.js';
59
60
  import { MenuItem } from './menu_item.js';
@@ -96,6 +97,12 @@ import {
96
97
  normalizeProductReviewApiJson,
97
98
  wireProductReviewCreateBody,
98
99
  } from './product_review.js';
100
+ import * as pricing from './pricing/index.js';
101
+
102
+ // Pure client-side pricing calculator (mirrors merchi_sdk_ts `pricing`):
103
+ // estimateQuote, resolveVisibleFields, resolveVisibleOptionIds,
104
+ // resolveUnavailableOptionIds, applyDiscount, roundHalfEven.
105
+ export { pricing };
99
106
 
100
107
  export function merchi(backendUri, websocketUri) {
101
108
  getGlobal().merchiJsonpHandlers = {};
@@ -1067,6 +1074,8 @@ export function merchi(backendUri, websocketUri) {
1067
1074
  'jobs': new Jobs(),
1068
1075
  'JobComment': JobComment,
1069
1076
  'JobNote': JobNote,
1077
+ 'JobOperationLog': JobOperationLog,
1078
+ 'jobOperationLogs': new JobOperationLogs(),
1070
1079
  'Cart': Cart,
1071
1080
  'CartItem': CartItem,
1072
1081
  'Bank': Bank,
@@ -0,0 +1,112 @@
1
+ {
2
+ "bundle": {
3
+ "currency": "AUD",
4
+ "fields": [
5
+ {
6
+ "fieldType": 2,
7
+ "id": 13,
8
+ "independent": true,
9
+ "isSelectable": true,
10
+ "options": [
11
+ {
12
+ "default": true,
13
+ "id": 9,
14
+ "originalId": null,
15
+ "position": 1,
16
+ "selectedBy": [],
17
+ "variationCost": 0.0,
18
+ "variationCostDiscountGroup": null,
19
+ "variationUnitCost": 0.0,
20
+ "variationUnitCostDiscountGroup": null
21
+ },
22
+ {
23
+ "default": false,
24
+ "id": 10,
25
+ "originalId": null,
26
+ "position": 2,
27
+ "selectedBy": [],
28
+ "variationCost": 0.0,
29
+ "variationCostDiscountGroup": null,
30
+ "variationUnitCost": 13.0,
31
+ "variationUnitCostDiscountGroup": null
32
+ }
33
+ ],
34
+ "originalId": null,
35
+ "position": 1,
36
+ "selectedBy": [],
37
+ "variationCost": 0.0,
38
+ "variationCostDiscountGroup": null,
39
+ "variationUnitCost": 0.0,
40
+ "variationUnitCostDiscountGroup": null
41
+ },
42
+ {
43
+ "fieldType": 1,
44
+ "id": 15,
45
+ "independent": true,
46
+ "isSelectable": false,
47
+ "options": [],
48
+ "originalId": null,
49
+ "position": 1,
50
+ "selectedBy": [
51
+ 10
52
+ ],
53
+ "variationCost": 0.0,
54
+ "variationCostDiscountGroup": null,
55
+ "variationUnitCost": 0.0,
56
+ "variationUnitCostDiscountGroup": null
57
+ }
58
+ ],
59
+ "groupFields": [
60
+ {
61
+ "fieldType": 3,
62
+ "id": 14,
63
+ "independent": false,
64
+ "isSelectable": false,
65
+ "options": [],
66
+ "originalId": null,
67
+ "position": 2,
68
+ "selectedBy": [],
69
+ "variationCost": 0.0,
70
+ "variationCostDiscountGroup": null,
71
+ "variationUnitCost": 0.0,
72
+ "variationUnitCostDiscountGroup": null
73
+ }
74
+ ],
75
+ "hasGroups": true,
76
+ "product": {
77
+ "discountGroup": {
78
+ "discounts": [
79
+ {
80
+ "amount": 10.0,
81
+ "lowerLimit": 400.0
82
+ }
83
+ ],
84
+ "groupRestricted": false
85
+ },
86
+ "minimumPrice": 0.0,
87
+ "unitPrice": 30.0
88
+ },
89
+ "taxPercent": 10.0
90
+ },
91
+ "expected": {
92
+ "cost": 150.0,
93
+ "costPerUnit": 30.0,
94
+ "groupCosts": [],
95
+ "taxAmount": 15.0,
96
+ "totalCost": 165.0,
97
+ "visibleFieldIds": [
98
+ 13
99
+ ]
100
+ },
101
+ "name": "conditional_field_hidden",
102
+ "selections": {
103
+ "fieldValues": {
104
+ "13": {
105
+ "selectedOptionIds": [
106
+ 9
107
+ ]
108
+ }
109
+ },
110
+ "quantity": 5
111
+ }
112
+ }
@@ -0,0 +1,113 @@
1
+ {
2
+ "bundle": {
3
+ "currency": "AUD",
4
+ "fields": [
5
+ {
6
+ "fieldType": 2,
7
+ "id": 13,
8
+ "independent": true,
9
+ "isSelectable": true,
10
+ "options": [
11
+ {
12
+ "default": true,
13
+ "id": 9,
14
+ "originalId": null,
15
+ "position": 1,
16
+ "selectedBy": [],
17
+ "variationCost": 0.0,
18
+ "variationCostDiscountGroup": null,
19
+ "variationUnitCost": 0.0,
20
+ "variationUnitCostDiscountGroup": null
21
+ },
22
+ {
23
+ "default": false,
24
+ "id": 10,
25
+ "originalId": null,
26
+ "position": 2,
27
+ "selectedBy": [],
28
+ "variationCost": 0.0,
29
+ "variationCostDiscountGroup": null,
30
+ "variationUnitCost": 13.0,
31
+ "variationUnitCostDiscountGroup": null
32
+ }
33
+ ],
34
+ "originalId": null,
35
+ "position": 1,
36
+ "selectedBy": [],
37
+ "variationCost": 0.0,
38
+ "variationCostDiscountGroup": null,
39
+ "variationUnitCost": 0.0,
40
+ "variationUnitCostDiscountGroup": null
41
+ },
42
+ {
43
+ "fieldType": 1,
44
+ "id": 15,
45
+ "independent": true,
46
+ "isSelectable": false,
47
+ "options": [],
48
+ "originalId": null,
49
+ "position": 1,
50
+ "selectedBy": [
51
+ 10
52
+ ],
53
+ "variationCost": 0.0,
54
+ "variationCostDiscountGroup": null,
55
+ "variationUnitCost": 0.0,
56
+ "variationUnitCostDiscountGroup": null
57
+ }
58
+ ],
59
+ "groupFields": [
60
+ {
61
+ "fieldType": 3,
62
+ "id": 14,
63
+ "independent": false,
64
+ "isSelectable": false,
65
+ "options": [],
66
+ "originalId": null,
67
+ "position": 2,
68
+ "selectedBy": [],
69
+ "variationCost": 0.0,
70
+ "variationCostDiscountGroup": null,
71
+ "variationUnitCost": 0.0,
72
+ "variationUnitCostDiscountGroup": null
73
+ }
74
+ ],
75
+ "hasGroups": true,
76
+ "product": {
77
+ "discountGroup": {
78
+ "discounts": [
79
+ {
80
+ "amount": 10.0,
81
+ "lowerLimit": 400.0
82
+ }
83
+ ],
84
+ "groupRestricted": false
85
+ },
86
+ "minimumPrice": 0.0,
87
+ "unitPrice": 30.0
88
+ },
89
+ "taxPercent": 10.0
90
+ },
91
+ "expected": {
92
+ "cost": 215.0,
93
+ "costPerUnit": 30.0,
94
+ "groupCosts": [],
95
+ "taxAmount": 21.5,
96
+ "totalCost": 236.5,
97
+ "visibleFieldIds": [
98
+ 13,
99
+ 15
100
+ ]
101
+ },
102
+ "name": "conditional_field_visible",
103
+ "selections": {
104
+ "fieldValues": {
105
+ "13": {
106
+ "selectedOptionIds": [
107
+ 10
108
+ ]
109
+ }
110
+ },
111
+ "quantity": 5
112
+ }
113
+ }
@@ -0,0 +1,130 @@
1
+ {
2
+ "bundle": {
3
+ "currency": "AUD",
4
+ "fields": [
5
+ {
6
+ "fieldType": 1,
7
+ "id": 12,
8
+ "independent": true,
9
+ "isSelectable": false,
10
+ "options": [],
11
+ "originalId": null,
12
+ "position": 1,
13
+ "selectedBy": [],
14
+ "variationCost": 11.0,
15
+ "variationCostDiscountGroup": null,
16
+ "variationUnitCost": 0.0,
17
+ "variationUnitCostDiscountGroup": null
18
+ }
19
+ ],
20
+ "groupFields": [
21
+ {
22
+ "fieldType": 2,
23
+ "id": 10,
24
+ "independent": false,
25
+ "isSelectable": true,
26
+ "options": [
27
+ {
28
+ "default": true,
29
+ "id": 7,
30
+ "originalId": null,
31
+ "position": 1,
32
+ "selectedBy": [],
33
+ "variationCost": 0.0,
34
+ "variationCostDiscountGroup": null,
35
+ "variationUnitCost": 0.0,
36
+ "variationUnitCostDiscountGroup": null
37
+ },
38
+ {
39
+ "default": false,
40
+ "id": 8,
41
+ "originalId": null,
42
+ "position": 2,
43
+ "selectedBy": [],
44
+ "variationCost": 0.0,
45
+ "variationCostDiscountGroup": null,
46
+ "variationUnitCost": 13.0,
47
+ "variationUnitCostDiscountGroup": null
48
+ }
49
+ ],
50
+ "originalId": null,
51
+ "position": 1,
52
+ "selectedBy": [],
53
+ "variationCost": 0.0,
54
+ "variationCostDiscountGroup": null,
55
+ "variationUnitCost": 0.0,
56
+ "variationUnitCostDiscountGroup": null
57
+ },
58
+ {
59
+ "fieldType": 3,
60
+ "id": 11,
61
+ "independent": false,
62
+ "isSelectable": false,
63
+ "options": [],
64
+ "originalId": null,
65
+ "position": 2,
66
+ "selectedBy": [],
67
+ "variationCost": 0.0,
68
+ "variationCostDiscountGroup": null,
69
+ "variationUnitCost": 0.0,
70
+ "variationUnitCostDiscountGroup": null
71
+ }
72
+ ],
73
+ "hasGroups": true,
74
+ "product": {
75
+ "discountGroup": {
76
+ "discounts": [
77
+ {
78
+ "amount": 10.0,
79
+ "lowerLimit": 400.0
80
+ }
81
+ ],
82
+ "groupRestricted": false
83
+ },
84
+ "minimumPrice": 0.0,
85
+ "unitPrice": 30.0
86
+ },
87
+ "taxPercent": 10.0
88
+ },
89
+ "expected": {
90
+ "cost": 645.0,
91
+ "costPerUnit": 30.0,
92
+ "groupCosts": [
93
+ 215.0,
94
+ 430.0
95
+ ],
96
+ "taxAmount": 64.5,
97
+ "totalCost": 709.5,
98
+ "visibleFieldIds": [
99
+ 10,
100
+ 11,
101
+ 12
102
+ ]
103
+ },
104
+ "name": "group_product",
105
+ "selections": {
106
+ "fieldValues": {},
107
+ "groups": [
108
+ {
109
+ "fieldValues": {
110
+ "10": {
111
+ "selectedOptionIds": [
112
+ 8
113
+ ]
114
+ }
115
+ },
116
+ "quantity": 5
117
+ },
118
+ {
119
+ "fieldValues": {
120
+ "10": {
121
+ "selectedOptionIds": [
122
+ 8
123
+ ]
124
+ }
125
+ },
126
+ "quantity": 10
127
+ }
128
+ ]
129
+ }
130
+ }
@@ -0,0 +1,108 @@
1
+ {
2
+ "bundle": {
3
+ "currency": "AUD",
4
+ "fields": [
5
+ {
6
+ "fieldType": 5,
7
+ "id": 9,
8
+ "independent": true,
9
+ "isSelectable": false,
10
+ "options": [],
11
+ "originalId": null,
12
+ "position": 1,
13
+ "selectedBy": [],
14
+ "variationCost": 25.0,
15
+ "variationCostDiscountGroup": null,
16
+ "variationUnitCost": 5.0,
17
+ "variationUnitCostDiscountGroup": null
18
+ }
19
+ ],
20
+ "groupFields": [
21
+ {
22
+ "fieldType": 2,
23
+ "id": 7,
24
+ "independent": false,
25
+ "isSelectable": true,
26
+ "options": [
27
+ {
28
+ "default": true,
29
+ "id": 5,
30
+ "originalId": null,
31
+ "position": 1,
32
+ "selectedBy": [],
33
+ "variationCost": 0.0,
34
+ "variationCostDiscountGroup": null,
35
+ "variationUnitCost": 0.0,
36
+ "variationUnitCostDiscountGroup": null
37
+ },
38
+ {
39
+ "default": false,
40
+ "id": 6,
41
+ "originalId": null,
42
+ "position": 2,
43
+ "selectedBy": [],
44
+ "variationCost": 0.0,
45
+ "variationCostDiscountGroup": null,
46
+ "variationUnitCost": 13.0,
47
+ "variationUnitCostDiscountGroup": null
48
+ }
49
+ ],
50
+ "originalId": null,
51
+ "position": 1,
52
+ "selectedBy": [],
53
+ "variationCost": 0.0,
54
+ "variationCostDiscountGroup": null,
55
+ "variationUnitCost": 0.0,
56
+ "variationUnitCostDiscountGroup": null
57
+ },
58
+ {
59
+ "fieldType": 3,
60
+ "id": 8,
61
+ "independent": false,
62
+ "isSelectable": false,
63
+ "options": [],
64
+ "originalId": null,
65
+ "position": 2,
66
+ "selectedBy": [],
67
+ "variationCost": 0.0,
68
+ "variationCostDiscountGroup": null,
69
+ "variationUnitCost": 0.0,
70
+ "variationUnitCostDiscountGroup": null
71
+ }
72
+ ],
73
+ "hasGroups": true,
74
+ "product": {
75
+ "discountGroup": {
76
+ "discounts": [
77
+ {
78
+ "amount": 10.0,
79
+ "lowerLimit": 400.0
80
+ }
81
+ ],
82
+ "groupRestricted": false
83
+ },
84
+ "minimumPrice": 0.0,
85
+ "unitPrice": 30.0
86
+ },
87
+ "taxPercent": 10.0
88
+ },
89
+ "expected": {
90
+ "cost": 375.0,
91
+ "costPerUnit": 30.0,
92
+ "groupCosts": [],
93
+ "taxAmount": 37.5,
94
+ "totalCost": 412.5,
95
+ "visibleFieldIds": [
96
+ 9
97
+ ]
98
+ },
99
+ "name": "non_selectable_number_field",
100
+ "selections": {
101
+ "fieldValues": {
102
+ "9": {
103
+ "value": "7"
104
+ }
105
+ },
106
+ "quantity": 10
107
+ }
108
+ }
@@ -0,0 +1,104 @@
1
+ {
2
+ "bundle": {
3
+ "currency": "AUD",
4
+ "fields": [
5
+ {
6
+ "fieldType": 1,
7
+ "id": 6,
8
+ "independent": true,
9
+ "isSelectable": false,
10
+ "options": [],
11
+ "originalId": null,
12
+ "position": 1,
13
+ "selectedBy": [],
14
+ "variationCost": 11.0,
15
+ "variationCostDiscountGroup": null,
16
+ "variationUnitCost": 0.0,
17
+ "variationUnitCostDiscountGroup": null
18
+ }
19
+ ],
20
+ "groupFields": [
21
+ {
22
+ "fieldType": 2,
23
+ "id": 4,
24
+ "independent": false,
25
+ "isSelectable": true,
26
+ "options": [
27
+ {
28
+ "default": true,
29
+ "id": 3,
30
+ "originalId": null,
31
+ "position": 1,
32
+ "selectedBy": [],
33
+ "variationCost": 0.0,
34
+ "variationCostDiscountGroup": null,
35
+ "variationUnitCost": 0.0,
36
+ "variationUnitCostDiscountGroup": null
37
+ },
38
+ {
39
+ "default": false,
40
+ "id": 4,
41
+ "originalId": null,
42
+ "position": 2,
43
+ "selectedBy": [],
44
+ "variationCost": 0.0,
45
+ "variationCostDiscountGroup": null,
46
+ "variationUnitCost": 13.0,
47
+ "variationUnitCostDiscountGroup": null
48
+ }
49
+ ],
50
+ "originalId": null,
51
+ "position": 1,
52
+ "selectedBy": [],
53
+ "variationCost": 0.0,
54
+ "variationCostDiscountGroup": null,
55
+ "variationUnitCost": 0.0,
56
+ "variationUnitCostDiscountGroup": null
57
+ },
58
+ {
59
+ "fieldType": 3,
60
+ "id": 5,
61
+ "independent": false,
62
+ "isSelectable": false,
63
+ "options": [],
64
+ "originalId": null,
65
+ "position": 2,
66
+ "selectedBy": [],
67
+ "variationCost": 0.0,
68
+ "variationCostDiscountGroup": null,
69
+ "variationUnitCost": 0.0,
70
+ "variationUnitCostDiscountGroup": null
71
+ }
72
+ ],
73
+ "hasGroups": true,
74
+ "product": {
75
+ "discountGroup": {
76
+ "discounts": [
77
+ {
78
+ "amount": 10.0,
79
+ "lowerLimit": 100.0
80
+ }
81
+ ],
82
+ "groupRestricted": false
83
+ },
84
+ "minimumPrice": 0.0,
85
+ "unitPrice": 20.0
86
+ },
87
+ "taxPercent": 10.0
88
+ },
89
+ "expected": {
90
+ "cost": 1800.0,
91
+ "costPerUnit": 18.0,
92
+ "groupCosts": [],
93
+ "taxAmount": 180.0,
94
+ "totalCost": 1980.0,
95
+ "visibleFieldIds": [
96
+ 6
97
+ ]
98
+ },
99
+ "name": "quantity_break_discount",
100
+ "selections": {
101
+ "fieldValues": {},
102
+ "quantity": 100
103
+ }
104
+ }
@@ -0,0 +1,110 @@
1
+ {
2
+ "bundle": {
3
+ "currency": "AUD",
4
+ "fields": [
5
+ {
6
+ "fieldType": 2,
7
+ "id": 1,
8
+ "independent": true,
9
+ "isSelectable": true,
10
+ "options": [
11
+ {
12
+ "default": true,
13
+ "id": 1,
14
+ "originalId": null,
15
+ "position": 1,
16
+ "selectedBy": [],
17
+ "variationCost": 50.0,
18
+ "variationCostDiscountGroup": null,
19
+ "variationUnitCost": 0.0,
20
+ "variationUnitCostDiscountGroup": null
21
+ },
22
+ {
23
+ "default": false,
24
+ "id": 2,
25
+ "originalId": null,
26
+ "position": 2,
27
+ "selectedBy": [],
28
+ "variationCost": 50.0,
29
+ "variationCostDiscountGroup": null,
30
+ "variationUnitCost": 13.0,
31
+ "variationUnitCostDiscountGroup": null
32
+ }
33
+ ],
34
+ "originalId": null,
35
+ "position": 1,
36
+ "selectedBy": [],
37
+ "variationCost": 0.0,
38
+ "variationCostDiscountGroup": null,
39
+ "variationUnitCost": 0.0,
40
+ "variationUnitCostDiscountGroup": null
41
+ }
42
+ ],
43
+ "groupFields": [
44
+ {
45
+ "fieldType": 1,
46
+ "id": 3,
47
+ "independent": false,
48
+ "isSelectable": false,
49
+ "options": [],
50
+ "originalId": null,
51
+ "position": 1,
52
+ "selectedBy": [],
53
+ "variationCost": 11.0,
54
+ "variationCostDiscountGroup": null,
55
+ "variationUnitCost": 0.0,
56
+ "variationUnitCostDiscountGroup": null
57
+ },
58
+ {
59
+ "fieldType": 3,
60
+ "id": 2,
61
+ "independent": false,
62
+ "isSelectable": false,
63
+ "options": [],
64
+ "originalId": null,
65
+ "position": 2,
66
+ "selectedBy": [],
67
+ "variationCost": 0.0,
68
+ "variationCostDiscountGroup": null,
69
+ "variationUnitCost": 0.0,
70
+ "variationUnitCostDiscountGroup": null
71
+ }
72
+ ],
73
+ "hasGroups": true,
74
+ "product": {
75
+ "discountGroup": {
76
+ "discounts": [
77
+ {
78
+ "amount": 10.0,
79
+ "lowerLimit": 400.0
80
+ }
81
+ ],
82
+ "groupRestricted": false
83
+ },
84
+ "minimumPrice": 0.0,
85
+ "unitPrice": 30.0
86
+ },
87
+ "taxPercent": 10.0
88
+ },
89
+ "expected": {
90
+ "cost": 350.0,
91
+ "costPerUnit": 30.0,
92
+ "groupCosts": [],
93
+ "taxAmount": 35.0,
94
+ "totalCost": 385.0,
95
+ "visibleFieldIds": [
96
+ 1
97
+ ]
98
+ },
99
+ "name": "simple_selectable_field",
100
+ "selections": {
101
+ "fieldValues": {
102
+ "1": {
103
+ "selectedOptionIds": [
104
+ 1
105
+ ]
106
+ }
107
+ },
108
+ "quantity": 10
109
+ }
110
+ }
@@ -0,0 +1,14 @@
1
+ import { roundHalfEven } from './round.js';
2
+
3
+ // Mirrors the server's discount_groups.get_discounted_price: pick the tier with
4
+ // the highest lowerLimit <= qty, apply price*(1 - amount/100), round to 3dp.
5
+ export function applyDiscount(price, qty, group) {
6
+ if (!group || group.discounts.length === 0) {
7
+ return roundHalfEven(price, 3);
8
+ }
9
+ const applicable = group.discounts
10
+ .filter((d) => d.lowerLimit <= qty)
11
+ .sort((a, b) => b.lowerLimit - a.lowerLimit);
12
+ const amount = applicable.length ? applicable[0].amount : 0;
13
+ return roundHalfEven(price * (1 - amount / 100), 3);
14
+ }
@@ -0,0 +1,172 @@
1
+ import { applyDiscount } from './discount.js';
2
+ import { roundHalfEven } from './round.js';
3
+ import { resolveVisibleFields } from './visibility.js';
4
+
5
+ function unitPriceAt(rules, qty) {
6
+ let unitPrice = applyDiscount(rules.product.unitPrice, qty, rules.product.discountGroup);
7
+ const mop = rules.product.minimumPrice;
8
+ if (qty > 0 && mop && unitPrice * qty < mop) {
9
+ unitPrice = mop / qty;
10
+ }
11
+ return unitPrice;
12
+ }
13
+
14
+ function variationSetupCost(source, totalQty) {
15
+ let cost = source.variationCost;
16
+ if (source.variationCostDiscountGroup) {
17
+ cost = applyDiscount(cost, totalQty, source.variationCostDiscountGroup);
18
+ }
19
+ return roundHalfEven(cost, 3);
20
+ }
21
+
22
+ function variationUnitCosts(source, groupQuantities) {
23
+ const uc = source.variationUnitCost;
24
+ const group = source.variationUnitCostDiscountGroup;
25
+ let list;
26
+ if (group) {
27
+ if (group.groupRestricted) {
28
+ list = groupQuantities.map((q) => applyDiscount(uc, q, group));
29
+ } else {
30
+ const total = groupQuantities.reduce((a, b) => a + b, 0);
31
+ const discounted = applyDiscount(uc, total, group);
32
+ list = groupQuantities.map(() => discounted);
33
+ }
34
+ } else {
35
+ list = groupQuantities.map(() => uc);
36
+ }
37
+ return list.map((c) => roundHalfEven(c, 3));
38
+ }
39
+
40
+ function isEmpty(field, sel) {
41
+ if (!sel) return true;
42
+ if (field.isSelectable) {
43
+ return !sel.selectedOptionIds || sel.selectedOptionIds.length === 0;
44
+ }
45
+ return (
46
+ sel.value === undefined ||
47
+ sel.value === null ||
48
+ sel.value === '' ||
49
+ sel.value === 0
50
+ );
51
+ }
52
+
53
+ function costFactor(field, sel, groupQuantities) {
54
+ const n = groupQuantities.length;
55
+ if (isEmpty(field, sel)) {
56
+ return { setup: 0, unitList: new Array(n).fill(0) };
57
+ }
58
+ const totalQty = groupQuantities.reduce((a, b) => a + b, 0);
59
+ if (field.isSelectable) {
60
+ let setup = 0;
61
+ const unitList = new Array(n).fill(0);
62
+ const optById = new Map();
63
+ for (const o of field.options) {
64
+ optById.set(o.id, o);
65
+ if (o.originalId != null) optById.set(o.originalId, o);
66
+ }
67
+ for (const optId of sel.selectedOptionIds) {
68
+ const opt = optById.get(optId);
69
+ if (!opt) continue;
70
+ setup += variationSetupCost(opt, totalQty);
71
+ const ucs = variationUnitCosts(opt, groupQuantities);
72
+ for (let i = 0; i < n; i++) unitList[i] += ucs[i];
73
+ }
74
+ return { setup, unitList };
75
+ }
76
+ return {
77
+ setup: variationSetupCost(field, totalQty),
78
+ unitList: variationUnitCosts(field, groupQuantities),
79
+ };
80
+ }
81
+
82
+ export function estimateQuote(rules, selections) {
83
+ if (rules.unsupported) {
84
+ return { unsupported: rules.unsupported };
85
+ }
86
+
87
+ const groupCosts = [];
88
+ let cost = 0;
89
+ let costPerUnit;
90
+ let groupQuantities;
91
+
92
+ // Mirror the server: the group-vs-single branch keys off whether the *job*
93
+ // actually has variation groups, not the product's capability.
94
+ const groups = selections.groups || [];
95
+ if (groups.length > 0) {
96
+ groupQuantities = groups.map((g) => g.quantity || 0);
97
+ const totalQty = groupQuantities.reduce((a, b) => a + b, 0);
98
+ const restricted = Boolean(
99
+ rules.product.discountGroup && rules.product.discountGroup.groupRestricted
100
+ );
101
+ const baseUnitPrice = unitPriceAt(rules, totalQty);
102
+ const perGroupCpu = [];
103
+
104
+ for (const group of groups) {
105
+ const gQty = group.quantity || 0;
106
+ const cpu = restricted ? unitPriceAt(rules, gQty) : baseUnitPrice;
107
+ perGroupCpu.push(cpu);
108
+ // A zero-qty group contributes exactly 0 (no field costs).
109
+ if (!gQty) {
110
+ groupCosts.push(0);
111
+ continue;
112
+ }
113
+ // Per-group field visibility is scoped to that group's selections plus
114
+ // the independent selections.
115
+ const groupVisible = resolveVisibleFields(rules, {
116
+ fieldValues: selections.fieldValues,
117
+ groups: [group],
118
+ });
119
+ let groupVariationCost = 0;
120
+ for (const field of rules.groupFields) {
121
+ if (!groupVisible.has(field.id)) continue;
122
+ const { setup, unitList } = costFactor(field, group.fieldValues[field.id], [gQty]);
123
+ groupVariationCost += setup + unitList[0] * gQty;
124
+ }
125
+ const groupCost = gQty * cpu + groupVariationCost;
126
+ groupCosts.push(groupCost);
127
+ cost += groupCost;
128
+ }
129
+
130
+ costPerUnit =
131
+ totalQty > 0
132
+ ? perGroupCpu.reduce((acc, cpu, i) => acc + cpu * groupQuantities[i], 0) / totalQty
133
+ : baseUnitPrice;
134
+
135
+ // Independent field visibility is scoped to independent selections only.
136
+ const independentVisible = resolveVisibleFields(rules, {
137
+ quantity: selections.quantity,
138
+ fieldValues: selections.fieldValues,
139
+ });
140
+ for (const field of rules.fields) {
141
+ if (!independentVisible.has(field.id)) continue;
142
+ const { setup, unitList } = costFactor(field, selections.fieldValues[field.id], groupQuantities);
143
+ const unitTotal = unitList.reduce((acc, uc, i) => acc + uc * groupQuantities[i], 0);
144
+ cost += setup + unitTotal;
145
+ }
146
+ } else {
147
+ const qty = selections.quantity || 0;
148
+ groupQuantities = [qty];
149
+ costPerUnit = unitPriceAt(rules, qty);
150
+ cost = costPerUnit * qty;
151
+ const visible = resolveVisibleFields(rules, selections);
152
+ for (const field of rules.fields) {
153
+ if (!visible.has(field.id)) continue;
154
+ const { setup, unitList } = costFactor(field, selections.fieldValues[field.id], groupQuantities);
155
+ cost += setup + unitList[0] * qty;
156
+ }
157
+ }
158
+
159
+ // Tax is derived from the UNROUNDED cost, then cost/tax rounded independently.
160
+ const unroundedCost = cost;
161
+ const roundedCost = roundHalfEven(unroundedCost, 2);
162
+ const unroundedTax = (unroundedCost * rules.taxPercent) / 100;
163
+ const taxAmount = roundHalfEven(unroundedTax, 2);
164
+ return {
165
+ costPerUnit: roundHalfEven(costPerUnit, 3),
166
+ cost: roundedCost,
167
+ taxAmount,
168
+ totalCost: roundHalfEven(unroundedCost + unroundedTax, 3),
169
+ groupCosts: groupCosts.map((c) => roundHalfEven(c, 3)),
170
+ currency: rules.currency,
171
+ };
172
+ }
@@ -0,0 +1,54 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { estimateQuote } from './estimate.js';
7
+ import { resolveVisibleFields } from './visibility.js';
8
+
9
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
10
+ const FIXTURES_DIR = path.join(HERE, '__fixtures__');
11
+
12
+ // Fixtures are generated by the merchi_api fixture generator from the real
13
+ // specialised-order-estimate endpoint (the same set vendored into
14
+ // merchi_sdk_ts), so both SDKs are validated against one server source.
15
+ function loadFixtures() {
16
+ if (!fs.existsSync(FIXTURES_DIR)) return [];
17
+ return fs
18
+ .readdirSync(FIXTURES_DIR)
19
+ .filter((f) => f.endsWith('.json'))
20
+ .map((f) => JSON.parse(fs.readFileSync(path.join(FIXTURES_DIR, f), 'utf-8')));
21
+ }
22
+
23
+ function closeTo(actual, expected, dp) {
24
+ const tolerance = 0.5 * Math.pow(10, -dp);
25
+ assert.ok(
26
+ Math.abs(actual - expected) < tolerance,
27
+ `expected ${actual} to be within ${tolerance} of ${expected}`
28
+ );
29
+ }
30
+
31
+ const fixtures = loadFixtures();
32
+
33
+ test('pricing fixtures exist', () => {
34
+ assert.ok(fixtures.length > 0, 'no fixtures found');
35
+ });
36
+
37
+ for (const fixture of fixtures) {
38
+ test(`golden parity: ${fixture.name} - estimateQuote matches server`, () => {
39
+ const result = estimateQuote(fixture.bundle, fixture.selections);
40
+ closeTo(result.cost, fixture.expected.cost, 2);
41
+ closeTo(result.costPerUnit, fixture.expected.costPerUnit, 3);
42
+ closeTo(result.taxAmount, fixture.expected.taxAmount, 2);
43
+ closeTo(result.totalCost, fixture.expected.totalCost, 2);
44
+ assert.equal(result.groupCosts.length, fixture.expected.groupCosts.length);
45
+ result.groupCosts.forEach((c, i) => closeTo(c, fixture.expected.groupCosts[i], 2));
46
+ });
47
+
48
+ test(`golden parity: ${fixture.name} - resolveVisibleFields matches server`, () => {
49
+ const visible = [...resolveVisibleFields(fixture.bundle, fixture.selections)].sort(
50
+ (a, b) => a - b
51
+ );
52
+ assert.deepEqual(visible, fixture.expected.visibleFieldIds);
53
+ });
54
+ }
@@ -0,0 +1,5 @@
1
+ export { roundHalfEven } from './round.js';
2
+ export { applyDiscount } from './discount.js';
3
+ export { resolveVisibleFields, resolveVisibleOptionIds } from './visibility.js';
4
+ export { resolveUnavailableOptionIds } from './inventory.js';
5
+ export { estimateQuote } from './estimate.js';
@@ -0,0 +1,73 @@
1
+ // Returns the set of option ids that should be DISABLED because the combination
2
+ // of (current inventory-relevant selections + that option) has no matching
3
+ // inventory with stock. Mirrors the server's subset match
4
+ // (inventories_subset_match_strategy): an inventory matches a selection when it
5
+ // contains every selected option; an option is available if any matching
6
+ // inventory has quantity > 0.
7
+ //
8
+ // Scope: pass the container's selections (independent fields use the top-level
9
+ // selections; a group uses its own + independent). Only options that appear in
10
+ // at least one inventory unit are inventory-tracked; all others are always
11
+ // available. Direct product inventories only (no shared inventory groups).
12
+ export function resolveUnavailableOptionIds(rules, selections) {
13
+ const unavailable = new Set();
14
+ const units = rules.inventoryUnits || [];
15
+ if (units.length === 0) return unavailable;
16
+
17
+ const allFields = [...rules.fields, ...rules.groupFields];
18
+
19
+ const canonical = new Map();
20
+ for (const f of allFields) {
21
+ for (const o of f.options) {
22
+ canonical.set(o.id, o.id);
23
+ if (o.originalId != null) canonical.set(o.originalId, o.id);
24
+ }
25
+ }
26
+ const canon = (id) => (canonical.has(id) ? canonical.get(id) : id);
27
+
28
+ const unitSets = units.map((u) => ({
29
+ quantity: u.quantity,
30
+ optionIds: new Set(u.optionIds.map(canon)),
31
+ }));
32
+ const inventoryOptionIds = new Set();
33
+ for (const u of unitSets) {
34
+ for (const id of u.optionIds) inventoryOptionIds.add(id);
35
+ }
36
+
37
+ const selectedByField = new Map();
38
+ const addSelections = (fieldValues) => {
39
+ for (const key of Object.keys(fieldValues)) {
40
+ const fieldId = Number(key);
41
+ const sel = fieldValues[fieldId];
42
+ if (!sel || !sel.selectedOptionIds) continue;
43
+ const set = selectedByField.get(fieldId) || new Set();
44
+ for (const oid of sel.selectedOptionIds) {
45
+ const c = canon(oid);
46
+ if (inventoryOptionIds.has(c)) set.add(c);
47
+ }
48
+ selectedByField.set(fieldId, set);
49
+ }
50
+ };
51
+ addSelections(selections.fieldValues || {});
52
+ for (const group of selections.groups || []) addSelections(group.fieldValues || {});
53
+
54
+ const hasGroups = Boolean(selections.groups && selections.groups.length > 0);
55
+ const fieldsToConsider = hasGroups ? allFields : rules.fields;
56
+
57
+ for (const f of fieldsToConsider) {
58
+ for (const o of f.options) {
59
+ const candidate = canon(o.id);
60
+ if (!inventoryOptionIds.has(candidate)) continue;
61
+ const filter = [candidate];
62
+ for (const [fieldId, set] of selectedByField) {
63
+ if (fieldId === f.id) continue;
64
+ for (const sid of set) filter.push(sid);
65
+ }
66
+ const available = unitSets.some(
67
+ (u) => u.quantity > 0 && filter.every((id) => u.optionIds.has(id))
68
+ );
69
+ if (!available) unavailable.add(o.id);
70
+ }
71
+ }
72
+ return unavailable;
73
+ }
@@ -0,0 +1,77 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { roundHalfEven } from './round.js';
4
+ import { applyDiscount } from './discount.js';
5
+ import { resolveUnavailableOptionIds } from './inventory.js';
6
+ import { estimateQuote } from './estimate.js';
7
+
8
+ test('roundHalfEven: decimal half-even', () => {
9
+ assert.equal(roundHalfEven(0.5, 0), 0);
10
+ assert.equal(roundHalfEven(1.5, 0), 2);
11
+ assert.equal(roundHalfEven(2.5, 0), 2);
12
+ assert.equal(roundHalfEven(2.675, 2), 2.68);
13
+ assert.equal(roundHalfEven(12.3456, 3), 12.346);
14
+ assert.equal(roundHalfEven(Infinity, 2), Infinity);
15
+ });
16
+
17
+ test('applyDiscount: highest applicable tier', () => {
18
+ const group = {
19
+ groupRestricted: false,
20
+ discounts: [
21
+ { lowerLimit: 100, amount: 5 },
22
+ { lowerLimit: 500, amount: 12 },
23
+ ],
24
+ };
25
+ assert.equal(applyDiscount(10, 50, null), 10);
26
+ assert.equal(applyDiscount(10, 50, group), 10);
27
+ assert.equal(applyDiscount(10, 100, group), 9.5);
28
+ assert.equal(applyDiscount(10, 500, group), 8.8);
29
+ });
30
+
31
+ test('estimateQuote: simple linear price + tax', () => {
32
+ const rules = {
33
+ currency: 'AUD',
34
+ taxPercent: 10,
35
+ product: { unitPrice: 10, minimumPrice: null, discountGroup: null },
36
+ fields: [],
37
+ groupFields: [],
38
+ hasGroups: false,
39
+ };
40
+ const r = estimateQuote(rules, { quantity: 5, fieldValues: {} });
41
+ assert.equal(r.cost, 50);
42
+ assert.equal(r.taxAmount, 5);
43
+ assert.equal(r.totalCost, 55);
44
+ });
45
+
46
+ test('resolveUnavailableOptionIds: 25mm makes Blue unavailable', () => {
47
+ const opt = (id) => ({
48
+ id, originalId: id, position: id, default: false,
49
+ variationCost: 0, variationUnitCost: 0,
50
+ variationCostDiscountGroup: null, variationUnitCostDiscountGroup: null,
51
+ selectedBy: [],
52
+ });
53
+ const f = (id, options) => ({
54
+ id, originalId: id, position: 0, fieldType: 2, independent: false,
55
+ isSelectable: true, selectedBy: [],
56
+ variationCost: 0, variationUnitCost: 0,
57
+ variationCostDiscountGroup: null, variationUnitCostDiscountGroup: null,
58
+ options: options.map(opt),
59
+ });
60
+ const rules = {
61
+ currency: 'AUD', taxPercent: 0,
62
+ product: { unitPrice: 0, minimumPrice: null, discountGroup: null },
63
+ fields: [], hasGroups: true, needsInventory: true,
64
+ groupFields: [f(800, [801, 802]), f(810, [811, 813])],
65
+ inventoryUnits: [
66
+ { optionIds: [801, 813], quantity: 5 }, // 24mm-Blue ok
67
+ { optionIds: [802, 811], quantity: 7 }, // 25mm-Red ok
68
+ { optionIds: [802, 813], quantity: 0 }, // 25mm-Blue OUT
69
+ ],
70
+ };
71
+ const u = resolveUnavailableOptionIds(rules, {
72
+ fieldValues: {},
73
+ groups: [{ quantity: 1, fieldValues: { 800: { selectedOptionIds: [802] } } }],
74
+ });
75
+ assert.equal(u.has(813), true);
76
+ assert.equal(u.has(811), false);
77
+ });
@@ -0,0 +1,19 @@
1
+ // Decimal half-even rounding (round half to even), mirroring the server's
2
+ // `Decimal(round(x, n))`. NOT float `round()` (e.g. roundHalfEven(2.675, 2) is
3
+ // 2.68, not 2.67). Targets the money domain (small magnitudes, dp <= 3); the
4
+ // EPS half-detection is tuned for that domain.
5
+ export function roundHalfEven(value, dp) {
6
+ if (!isFinite(value)) return value;
7
+ const factor = Math.pow(10, dp);
8
+ const scaled = value * factor;
9
+ const floor = Math.floor(scaled);
10
+ const diff = scaled - floor;
11
+ const EPS = 1e-9;
12
+ let rounded;
13
+ if (Math.abs(diff - 0.5) < EPS) {
14
+ rounded = floor % 2 === 0 ? floor : floor + 1;
15
+ } else {
16
+ rounded = Math.round(scaled);
17
+ }
18
+ return rounded / factor;
19
+ }
@@ -0,0 +1,90 @@
1
+ function collectSelectedOptionIds(selections) {
2
+ const ids = [];
3
+ const addFrom = (fieldValues) => {
4
+ for (const key of Object.keys(fieldValues)) {
5
+ const sel = fieldValues[Number(key)];
6
+ if (sel && sel.selectedOptionIds) ids.push(...sel.selectedOptionIds);
7
+ }
8
+ };
9
+ addFrom(selections.fieldValues || {});
10
+ for (const group of selections.groups || []) addFrom(group.fieldValues || {});
11
+ return ids;
12
+ }
13
+
14
+ // Shared machinery for field- and option-level conditional visibility. Mirrors
15
+ // the server's `check_selected_by_fullfilled` recursion: a `selectedBy` list is
16
+ // fulfilled when any of its trigger option ids is currently selected AND that
17
+ // selecting option is itself fulfilled (its own `selectedBy` and its owning
18
+ // field's `selectedBy`), with a `checked` accumulator to break cycles.
19
+ function buildResolver(rules, selections) {
20
+ const allFields = [...rules.fields, ...rules.groupFields];
21
+ const optionsById = new Map();
22
+ const fieldByOptionId = new Map();
23
+ for (const f of allFields) {
24
+ for (const o of f.options) {
25
+ optionsById.set(o.id, o);
26
+ fieldByOptionId.set(o.id, f);
27
+ if (o.originalId != null) {
28
+ optionsById.set(o.originalId, o);
29
+ fieldByOptionId.set(o.originalId, f);
30
+ }
31
+ }
32
+ }
33
+
34
+ const selectedOptionIds = collectSelectedOptionIds(selections);
35
+
36
+ const sameOption = (a, b) => {
37
+ const oa = optionsById.get(a);
38
+ return oa != null && oa === optionsById.get(b);
39
+ };
40
+
41
+ const isFulfilled = (selectedBy, checked) => {
42
+ if (!selectedBy || selectedBy.length === 0) return true;
43
+ for (const triggerId of selectedBy) {
44
+ for (const selId of selectedOptionIds) {
45
+ if (!sameOption(triggerId, selId)) continue;
46
+ if (checked.includes(selId)) return true;
47
+ const opt = optionsById.get(selId);
48
+ const field = fieldByOptionId.get(selId);
49
+ const nextChecked = [...checked, selId];
50
+ if (
51
+ isFulfilled(opt.selectedBy, nextChecked) &&
52
+ isFulfilled(field.selectedBy, nextChecked)
53
+ ) {
54
+ return true;
55
+ }
56
+ }
57
+ }
58
+ return false;
59
+ };
60
+
61
+ // Group fields only materialise as variations when the job actually has
62
+ // groups (mirrors the server: a non-group job exposes no group fields).
63
+ const hasGroups = Boolean(selections.groups && selections.groups.length > 0);
64
+ const fieldsToConsider = hasGroups ? allFields : rules.fields;
65
+
66
+ return { allFields, fieldsToConsider, isFulfilled };
67
+ }
68
+
69
+ export function resolveVisibleFields(rules, selections) {
70
+ const { fieldsToConsider, isFulfilled } = buildResolver(rules, selections);
71
+ const visible = new Set();
72
+ for (const f of fieldsToConsider) {
73
+ if (isFulfilled(f.selectedBy, [])) visible.add(f.id);
74
+ }
75
+ return visible;
76
+ }
77
+
78
+ // Option ids whose own `selectedBy` is fulfilled (i.e. options that should be
79
+ // visible/selectable). Mirrors the server's per-option
80
+ // `isVisible = check_selected_by_fullfilled(option.selected_by)`.
81
+ export function resolveVisibleOptionIds(rules, selections) {
82
+ const { fieldsToConsider, isFulfilled } = buildResolver(rules, selections);
83
+ const visible = new Set();
84
+ for (const f of fieldsToConsider) {
85
+ for (const o of f.options) {
86
+ if (isFulfilled(o.selectedBy, [])) visible.add(o.id);
87
+ }
88
+ }
89
+ return visible;
90
+ }
package/src/user.js CHANGED
@@ -18,6 +18,7 @@ import { UserCompany } from './user_company.js';
18
18
  import { Reminder } from './reminder.js';
19
19
  import { InternalTag } from './internal_tag.js';
20
20
  import { DomainTag } from './domain_tag.js';
21
+ import { JobOperationLog } from './job_operation_log.js';
21
22
 
22
23
  export function User() {
23
24
  this.resource = '/users';
@@ -62,6 +63,7 @@ export function User() {
62
63
  addPropertyTo(this, 'tags', DomainTag);
63
64
  addPropertyTo(this, 'internalTags', InternalTag);
64
65
  addPropertyTo(this, 'reminders', Reminder);
66
+ addPropertyTo(this, 'jobOperationLogs', JobOperationLog);
65
67
 
66
68
  this.create = function (success, error, embed, as_domain) {
67
69
  var self = this,