@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
- ```typescript
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
- // Propose a quote
29
- await quotationsModule.propose(quotationId, {
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
- // Find quotations
35
- const quotations = await quotationsModule.findQuotations({
36
- status: QuotationStatus.PROPOSED,
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
- ## API Overview
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
- | Event | Description |
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, isDocumentDBCompatModeEnabled, } from '@unchainedshop/mongodb';
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
- if (!isDocumentDBCompatModeEnabled()) {
12
- await buildDbIndexes(Quotations, [
13
- {
14
- index: {
15
- _id: 'text',
16
- userId: 'text',
17
- quotationNumber: 'text',
18
- status: 'text',
19
- },
20
- options: {
21
- weights: {
22
- _id: 8,
23
- userId: 3,
24
- quotationNumber: 6,
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: Date;
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, values: any) => Promise<mongodb.WithId<Quotation> | null>;
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, assertDocumentDBCompatMode, } from '@unchainedshop/mongodb';
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 = { productId };
100
- selector.status = { $in: [QuotationStatus.REQUESTED, QuotationStatus.PROPOSED] };
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 { deletedCount } = await Quotations.deleteMany({
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: updateQuotationFields(['price', 'expires', 'meta']),
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.1",
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.1",
39
- "@unchainedshop/logger": "^5.0.0-alpha.1",
40
- "@unchainedshop/utils": "^5.0.0-alpha.1"
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": "^25.0.0",
44
- "typescript": "^5.8.3"
46
+ "@types/node": "^26.2.0",
47
+ "typescript": "^6.0.3"
45
48
  }
46
49
  }