@unchainedshop/core-quotations 5.0.0-alpha.1 → 5.0.0-alpha.11
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
CHANGED
|
@@ -13,89 +13,21 @@ npm install @unchainedshop/core-quotations
|
|
|
13
13
|
|
|
14
14
|
## Usage
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
import { configureQuotationsModule, QuotationStatus } from '@unchainedshop/core-quotations';
|
|
18
|
-
|
|
19
|
-
const quotationsModule = await configureQuotationsModule({ db });
|
|
20
|
-
|
|
21
|
-
// Create a quotation request
|
|
22
|
-
const quotationId = await quotationsModule.create({
|
|
23
|
-
userId: 'user-123',
|
|
24
|
-
productId: 'custom-product-456',
|
|
25
|
-
configuration: [{ key: 'quantity', value: '1000' }],
|
|
26
|
-
});
|
|
16
|
+
The platform initializes this module as `platform.unchainedAPI.modules.quotations`.
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
price: { amount: 5000, currency: 'CHF' },
|
|
31
|
-
expiresAt: new Date('2024-12-31'),
|
|
32
|
-
});
|
|
18
|
+
```typescript
|
|
19
|
+
import { QuotationStatus } from '@unchainedshop/core-quotations';
|
|
33
20
|
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
21
|
+
const { quotations } = platform.unchainedAPI.modules;
|
|
22
|
+
const userQuotations = await quotations.findQuotations({ userId: 'user-123' });
|
|
23
|
+
const proposals = userQuotations.filter(
|
|
24
|
+
(quotation) => quotations.normalizedStatus(quotation) === QuotationStatus.PROPOSED,
|
|
25
|
+
);
|
|
38
26
|
```
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
### Module Configuration
|
|
43
|
-
|
|
44
|
-
| Export | Description |
|
|
45
|
-
|--------|-------------|
|
|
46
|
-
| `configureQuotationsModule` | Configure and return the quotations module |
|
|
47
|
-
|
|
48
|
-
### Queries
|
|
49
|
-
|
|
50
|
-
| Method | Description |
|
|
51
|
-
|--------|-------------|
|
|
52
|
-
| `findQuotation` | Find quotation by ID |
|
|
53
|
-
| `findQuotations` | Find quotations with filtering and pagination |
|
|
54
|
-
| `count` | Count quotations matching query |
|
|
55
|
-
| `quotationExists` | Check if quotation exists |
|
|
56
|
-
|
|
57
|
-
### Mutations
|
|
58
|
-
|
|
59
|
-
| Method | Description |
|
|
60
|
-
|--------|-------------|
|
|
61
|
-
| `create` | Create a quotation request |
|
|
62
|
-
| `update` | Update quotation data |
|
|
63
|
-
| `delete` | Delete a quotation |
|
|
64
|
-
| `propose` | Propose a quote |
|
|
65
|
-
| `verify` | Verify a quotation |
|
|
66
|
-
| `reject` | Reject a quotation |
|
|
67
|
-
| `fulfill` | Mark quotation as fulfilled |
|
|
68
|
-
|
|
69
|
-
### Constants
|
|
70
|
-
|
|
71
|
-
| Export | Description |
|
|
72
|
-
|--------|-------------|
|
|
73
|
-
| `QuotationStatus` | Status values (REQUESTED, PROCESSING, PROPOSED, FULFILLED, REJECTED) |
|
|
74
|
-
|
|
75
|
-
### Settings
|
|
76
|
-
|
|
77
|
-
| Export | Description |
|
|
78
|
-
|--------|-------------|
|
|
79
|
-
| `quotationsSettings` | Access quotation module settings |
|
|
80
|
-
|
|
81
|
-
### Types
|
|
82
|
-
|
|
83
|
-
| Export | Description |
|
|
84
|
-
|--------|-------------|
|
|
85
|
-
| `Quotation` | Quotation document type |
|
|
86
|
-
| `QuotationConfiguration` | Configuration item type |
|
|
87
|
-
| `QuotationsModule` | Module interface type |
|
|
88
|
-
|
|
89
|
-
## Events
|
|
28
|
+
Use the GraphQL quotation mutations or `unchainedAPI.services.quotations` to request, propose, and verify quotations through the configured adapter. A proposal's `price` is an integer amount in the quotation currency's minor units, and its expiration field is `expires`.
|
|
90
29
|
|
|
91
|
-
|
|
92
|
-
|-------|-------------|
|
|
93
|
-
| `QUOTATION_CREATE` | Quotation requested |
|
|
94
|
-
| `QUOTATION_UPDATE` | Quotation updated |
|
|
95
|
-
| `QUOTATION_REMOVE` | Quotation deleted |
|
|
96
|
-
| `QUOTATION_PROPOSE` | Quote proposed |
|
|
97
|
-
| `QUOTATION_REJECT` | Quotation rejected |
|
|
98
|
-
| `QUOTATION_FULLFILL` | Quotation fulfilled |
|
|
30
|
+
See the [module guide](https://docs.unchained.shop/platform-configuration/modules/quotations), [quotation extension guide](https://docs.unchained.shop/extend/quotation), [public exports](src/quotations-index.ts), and [module implementation](src/module/configureQuotationsModule.ts).
|
|
99
31
|
|
|
100
32
|
## License
|
|
101
33
|
|
|
@@ -9,6 +9,9 @@ export declare const QuotationStatus: {
|
|
|
9
9
|
export type QuotationStatus = (typeof QuotationStatus)[keyof typeof QuotationStatus];
|
|
10
10
|
export interface QuotationProposal {
|
|
11
11
|
price?: number;
|
|
12
|
+
quantity?: number;
|
|
13
|
+
isTaxable?: boolean;
|
|
14
|
+
isNetPrice?: boolean;
|
|
12
15
|
expires?: Date;
|
|
13
16
|
meta?: any;
|
|
14
17
|
}
|
|
@@ -30,9 +33,12 @@ export type Quotation = {
|
|
|
30
33
|
currencyCode?: string;
|
|
31
34
|
expires?: Date;
|
|
32
35
|
fulfilled?: Date;
|
|
36
|
+
isNetPrice?: boolean;
|
|
37
|
+
isTaxable?: boolean;
|
|
33
38
|
meta?: any;
|
|
34
39
|
price?: number;
|
|
35
40
|
productId: string;
|
|
41
|
+
quantity?: number;
|
|
36
42
|
quotationNumber?: string;
|
|
37
43
|
rejected?: Date;
|
|
38
44
|
status: string | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mongodb, buildDbIndexes
|
|
1
|
+
import { mongodb, buildDbIndexes } from '@unchainedshop/mongodb';
|
|
2
2
|
export const QuotationStatus = {
|
|
3
3
|
REQUESTED: 'REQUESTED',
|
|
4
4
|
PROCESSING: 'PROCESSING',
|
|
@@ -8,30 +8,28 @@ export const QuotationStatus = {
|
|
|
8
8
|
};
|
|
9
9
|
export const QuotationsCollection = async (db) => {
|
|
10
10
|
const Quotations = db.collection('quotations');
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
status: 1,
|
|
26
|
-
},
|
|
27
|
-
name: 'quotation_fulltext_search',
|
|
11
|
+
await buildDbIndexes(Quotations, [
|
|
12
|
+
{
|
|
13
|
+
index: {
|
|
14
|
+
_id: 'text',
|
|
15
|
+
userId: 'text',
|
|
16
|
+
quotationNumber: 'text',
|
|
17
|
+
status: 'text',
|
|
18
|
+
},
|
|
19
|
+
options: {
|
|
20
|
+
weights: {
|
|
21
|
+
_id: 8,
|
|
22
|
+
userId: 3,
|
|
23
|
+
quotationNumber: 6,
|
|
24
|
+
status: 1,
|
|
28
25
|
},
|
|
26
|
+
name: 'quotation_fulltext_search',
|
|
29
27
|
},
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
},
|
|
29
|
+
]);
|
|
32
30
|
await buildDbIndexes(Quotations, [
|
|
33
|
-
{ index: { userId: 1 } },
|
|
34
|
-
{ index: { productId: 1 } },
|
|
31
|
+
{ index: { userId: 1, status: 1 } },
|
|
32
|
+
{ index: { productId: 1, status: 1 } },
|
|
35
33
|
{ index: { status: 1 } },
|
|
36
34
|
]);
|
|
37
35
|
return Quotations;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SortOption } from '@unchainedshop/utils';
|
|
2
|
-
import { type Quotation, QuotationStatus } from '../db/QuotationsCollection.ts';
|
|
2
|
+
import { type Quotation, type QuotationProposal, QuotationStatus } from '../db/QuotationsCollection.ts';
|
|
3
3
|
import { mongodb, type ModuleInput } from '@unchainedshop/mongodb';
|
|
4
4
|
import { type QuotationsSettingsOptions } from '../quotations-settings.ts';
|
|
5
5
|
export interface QuotationQuery {
|
|
@@ -14,6 +14,7 @@ export interface QuotationData {
|
|
|
14
14
|
}[];
|
|
15
15
|
countryCode?: string;
|
|
16
16
|
productId: string;
|
|
17
|
+
quantity?: number;
|
|
17
18
|
userId: string;
|
|
18
19
|
}
|
|
19
20
|
export declare const buildFindSelector: ({ userId, queryString, quotationIds }?: QuotationQuery) => mongodb.Filter<Quotation>;
|
|
@@ -31,16 +32,22 @@ export declare const configureQuotationsModule: ({ db, migrationRepository, opti
|
|
|
31
32
|
sort?: SortOption[];
|
|
32
33
|
}, options?: mongodb.FindOptions) => Promise<Quotation[]>;
|
|
33
34
|
normalizedStatus: (quotation: Quotation) => QuotationStatus;
|
|
34
|
-
isExpired(quotation: Quotation, { referenceDate }
|
|
35
|
-
referenceDate
|
|
35
|
+
isExpired(quotation: Quotation, { referenceDate }?: {
|
|
36
|
+
referenceDate?: Date;
|
|
36
37
|
}): boolean;
|
|
37
38
|
isProposalValid(quotation: Quotation): boolean;
|
|
39
|
+
isProposalValidFor(quotation: Quotation, { userId, productId, currencyCode }: {
|
|
40
|
+
userId: string;
|
|
41
|
+
productId: string;
|
|
42
|
+
currencyCode: string;
|
|
43
|
+
}): boolean;
|
|
38
44
|
create: ({ countryCode, currencyCode, ...quotationData }: QuotationData & {
|
|
39
45
|
currencyCode: string;
|
|
40
46
|
}) => Promise<Quotation>;
|
|
41
47
|
deleteRequestedUserQuotations: (userId: string) => Promise<number>;
|
|
48
|
+
replaceUserIdOfOpenQuotations: (fromUserId: string, toUserId: string) => Promise<number>;
|
|
42
49
|
updateContext: (quotationId: string, values: any) => Promise<mongodb.WithId<Quotation> | null>;
|
|
43
|
-
updateProposal: (quotationId: string,
|
|
50
|
+
updateProposal: (quotationId: string, proposal: QuotationProposal) => Promise<mongodb.WithId<Quotation> | null>;
|
|
44
51
|
updateStatus: (quotationId: string, { status, info }: {
|
|
45
52
|
status: QuotationStatus;
|
|
46
53
|
info?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SortDirection } from '@unchainedshop/utils';
|
|
2
2
|
import { QuotationStatus } from "../db/QuotationsCollection.js";
|
|
3
3
|
import { emit, registerEvents } from '@unchainedshop/events';
|
|
4
|
-
import { generateDbFilterById, buildSortOptions, mongodb, generateDbObjectId,
|
|
4
|
+
import { generateDbFilterById, buildSortOptions, mongodb, generateDbObjectId, } from '@unchainedshop/mongodb';
|
|
5
5
|
import { QuotationsCollection } from "../db/QuotationsCollection.js";
|
|
6
6
|
import { quotationsSettings } from "../quotations-settings.js";
|
|
7
7
|
import renameCurrencyCode from "../migrations/20250502111800-currency-code.js";
|
|
@@ -15,7 +15,6 @@ export const buildFindSelector = ({ userId, queryString, quotationIds } = {}) =>
|
|
|
15
15
|
selector._id = { $in: quotationIds };
|
|
16
16
|
}
|
|
17
17
|
if (queryString) {
|
|
18
|
-
assertDocumentDBCompatMode();
|
|
19
18
|
selector.$text = { $search: queryString };
|
|
20
19
|
}
|
|
21
20
|
return selector;
|
|
@@ -84,7 +83,7 @@ export const configureQuotationsModule = async ({ db, migrationRepository, optio
|
|
|
84
83
|
updated: new Date(),
|
|
85
84
|
...fieldKeys.reduce((set, key) => ({ ...set, [key]: values[key] }), {}),
|
|
86
85
|
},
|
|
87
|
-
});
|
|
86
|
+
}, { returnDocument: 'after' });
|
|
88
87
|
if (!quotation)
|
|
89
88
|
return null;
|
|
90
89
|
await emit('QUOTATION_UPDATE', { quotation, fields: fieldKeys });
|
|
@@ -96,8 +95,16 @@ export const configureQuotationsModule = async ({ db, migrationRepository, optio
|
|
|
96
95
|
return quotationCount;
|
|
97
96
|
},
|
|
98
97
|
openQuotationWithProduct: async ({ productId }) => {
|
|
99
|
-
const selector = {
|
|
100
|
-
|
|
98
|
+
const selector = {
|
|
99
|
+
productId,
|
|
100
|
+
$or: [
|
|
101
|
+
{ status: QuotationStatus.REQUESTED },
|
|
102
|
+
{
|
|
103
|
+
status: QuotationStatus.PROPOSED,
|
|
104
|
+
$or: [{ expires: { $not: { $type: 'date' } } }, { expires: { $gt: new Date() } }],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
101
108
|
return Quotations.findOne(selector);
|
|
102
109
|
},
|
|
103
110
|
findQuotation: async ({ quotationId }, options) => {
|
|
@@ -119,7 +126,7 @@ export const configureQuotationsModule = async ({ db, migrationRepository, optio
|
|
|
119
126
|
? QuotationStatus.REQUESTED
|
|
120
127
|
: quotation.status;
|
|
121
128
|
},
|
|
122
|
-
isExpired(quotation, { referenceDate }) {
|
|
129
|
+
isExpired(quotation, { referenceDate } = {}) {
|
|
123
130
|
const relevantDate = referenceDate ? new Date(referenceDate) : new Date();
|
|
124
131
|
if (!quotation.expires)
|
|
125
132
|
return false;
|
|
@@ -130,6 +137,12 @@ export const configureQuotationsModule = async ({ db, migrationRepository, optio
|
|
|
130
137
|
isProposalValid(quotation) {
|
|
131
138
|
return quotation.status === QuotationStatus.PROPOSED && !this.isExpired(quotation);
|
|
132
139
|
},
|
|
140
|
+
isProposalValidFor(quotation, { userId, productId, currencyCode }) {
|
|
141
|
+
return (this.isProposalValid(quotation) &&
|
|
142
|
+
quotation.userId === userId &&
|
|
143
|
+
quotation.productId === productId &&
|
|
144
|
+
quotation.currencyCode === currencyCode);
|
|
145
|
+
},
|
|
133
146
|
create: async ({ countryCode, currencyCode, ...quotationData }) => {
|
|
134
147
|
const { insertedId: quotationId } = await Quotations.insertOne({
|
|
135
148
|
_id: generateDbObjectId(),
|
|
@@ -146,14 +159,33 @@ export const configureQuotationsModule = async ({ db, migrationRepository, optio
|
|
|
146
159
|
return quotation;
|
|
147
160
|
},
|
|
148
161
|
deleteRequestedUserQuotations: async (userId) => {
|
|
149
|
-
const
|
|
162
|
+
const selector = {
|
|
150
163
|
userId,
|
|
151
164
|
status: { $in: [QuotationStatus.REQUESTED, null] },
|
|
152
|
-
}
|
|
165
|
+
};
|
|
166
|
+
const quotations = await Quotations.find(selector, { projection: { _id: 1 } }).toArray();
|
|
167
|
+
const { deletedCount } = await Quotations.deleteMany(selector);
|
|
168
|
+
await Promise.all(quotations.map((quotation) => emit('QUOTATION_REMOVE', { quotationId: quotation._id })));
|
|
153
169
|
return deletedCount;
|
|
154
170
|
},
|
|
171
|
+
replaceUserIdOfOpenQuotations: async (fromUserId, toUserId) => {
|
|
172
|
+
const result = await Quotations.updateMany({
|
|
173
|
+
userId: fromUserId,
|
|
174
|
+
status: {
|
|
175
|
+
$in: [null, QuotationStatus.REQUESTED, QuotationStatus.PROCESSING, QuotationStatus.PROPOSED],
|
|
176
|
+
},
|
|
177
|
+
}, { $set: { userId: toUserId, updated: new Date() } });
|
|
178
|
+
return result.modifiedCount;
|
|
179
|
+
},
|
|
155
180
|
updateContext: updateQuotationFields(['context']),
|
|
156
|
-
updateProposal:
|
|
181
|
+
updateProposal: (quotationId, proposal) => updateQuotationFields([
|
|
182
|
+
'price',
|
|
183
|
+
'isTaxable',
|
|
184
|
+
'isNetPrice',
|
|
185
|
+
'expires',
|
|
186
|
+
'meta',
|
|
187
|
+
...(Number.isInteger(proposal.quantity) && proposal.quantity > 0 ? ['quantity'] : []),
|
|
188
|
+
])(quotationId, proposal),
|
|
157
189
|
updateStatus,
|
|
158
190
|
};
|
|
159
191
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unchainedshop/core-quotations",
|
|
3
3
|
"description": "Quotation and RFQ management module for the Unchained Engine",
|
|
4
|
-
"version": "5.0.0-alpha.
|
|
4
|
+
"version": "5.0.0-alpha.11",
|
|
5
|
+
"files": [
|
|
6
|
+
"lib"
|
|
7
|
+
],
|
|
5
8
|
"main": "lib/quotations-index.js",
|
|
6
9
|
"types": "lib/quotations-index.d.ts",
|
|
7
10
|
"type": "module",
|
|
@@ -35,12 +38,12 @@
|
|
|
35
38
|
},
|
|
36
39
|
"homepage": "https://github.com/unchainedshop/unchained#readme",
|
|
37
40
|
"dependencies": {
|
|
38
|
-
"@unchainedshop/events": "^5.0.0-alpha.
|
|
39
|
-
"@unchainedshop/
|
|
40
|
-
"@unchainedshop/utils": "^5.0.0-alpha.
|
|
41
|
+
"@unchainedshop/events": "^5.0.0-alpha.11",
|
|
42
|
+
"@unchainedshop/mongodb": "^5.0.0-alpha.11",
|
|
43
|
+
"@unchainedshop/utils": "^5.0.0-alpha.11"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^
|
|
44
|
-
"typescript": "^
|
|
46
|
+
"@types/node": "^26.2.0",
|
|
47
|
+
"typescript": "^6.0.3"
|
|
45
48
|
}
|
|
46
49
|
}
|