@unchainedshop/core-orders 4.4.0 → 4.5.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.
@@ -26,7 +26,7 @@ export type Order = {
26
26
  status: OrderStatus | null;
27
27
  userId: string;
28
28
  } & LogFields & TimestampFields;
29
- export interface OrderQuery extends mongodb.Filter<Order> {
29
+ export interface OrderQuery {
30
30
  includeCarts?: boolean;
31
31
  queryString?: string;
32
32
  status?: OrderStatus[];
@@ -34,5 +34,8 @@ export interface OrderQuery extends mongodb.Filter<Order> {
34
34
  deliveryIds?: string[];
35
35
  paymentIds?: string[];
36
36
  dateRange?: DateFilterInput;
37
+ orderIds?: string[];
38
+ paymentProviderIds?: string[];
39
+ deliveryProviderIds?: string[];
37
40
  }
38
41
  export declare const OrdersCollection: (db: mongodb.Db) => Promise<mongodb.Collection<Order>>;
@@ -1,4 +1,4 @@
1
1
  import type { OrderQuery, Order } from '../db/OrdersCollection.ts';
2
2
  import { mongodb } from '@unchainedshop/mongodb';
3
- export declare const buildFindSelector: ({ includeCarts, status, userId, queryString, paymentIds, deliveryIds, dateRange, ...rest }: OrderQuery) => mongodb.Filter<Order>;
3
+ export declare const buildFindSelector: ({ includeCarts, status, userId, queryString, paymentIds, deliveryIds, dateRange, orderIds, }: OrderQuery) => mongodb.Filter<Order>;
4
4
  export default buildFindSelector;
@@ -1,9 +1,12 @@
1
1
  import { assertDocumentDBCompatMode, mongodb } from '@unchainedshop/mongodb';
2
- export const buildFindSelector = ({ includeCarts, status, userId, queryString, paymentIds, deliveryIds, dateRange, ...rest }) => {
3
- const selector = { ...rest };
2
+ export const buildFindSelector = ({ includeCarts, status, userId, queryString, paymentIds, deliveryIds, dateRange, orderIds, }) => {
3
+ const selector = {};
4
4
  if (userId) {
5
5
  selector.userId = userId;
6
6
  }
7
+ if (orderIds) {
8
+ selector._id = { $in: orderIds };
9
+ }
7
10
  if (dateRange) {
8
11
  const dateFilter = {};
9
12
  if (dateRange.start)
@@ -11,6 +11,11 @@ export interface OrderPositionAggregateParams {
11
11
  limit?: number;
12
12
  pipeline?: mongodb.Document[];
13
13
  }
14
+ export interface TopProductRecord {
15
+ productId: string;
16
+ totalSold: number;
17
+ totalRevenue: number;
18
+ }
14
19
  export declare const buildFindOrderPositionByIdSelector: (orderPositionId: string, orderId?: string) => mongodb.Filter<OrderPosition>;
15
20
  export declare const configureOrderPositionsModule: ({ OrderPositions, }: {
16
21
  OrderPositions: mongodb.Collection<OrderPosition>;
@@ -50,5 +55,8 @@ export declare const configureOrderPositionsModule: ({ OrderPositions, }: {
50
55
  }) => Promise<OrderPosition>;
51
56
  deleteOrderPositions: (orderId: string) => Promise<number>;
52
57
  aggregatePositions: ({ match, project, group, addFields, sort, limit, pipeline, }: OrderPositionAggregateParams) => Promise<mongodb.Document[]>;
58
+ getTopProducts: (orderIds: string[], options?: {
59
+ limit?: number;
60
+ }) => Promise<TopProductRecord[]>;
53
61
  };
54
62
  export type OrderPositionsModule = ReturnType<typeof configureOrderPositionsModule>;
@@ -151,5 +151,52 @@ export const configureOrderPositionsModule = ({ OrderPositions, }) => {
151
151
  stages.push({ $limit: limit });
152
152
  return OrderPositions.aggregate(stages).toArray();
153
153
  },
154
+ getTopProducts: async (orderIds, options) => {
155
+ const limit = options?.limit || 10;
156
+ const pipeline = [
157
+ { $match: { orderId: { $in: orderIds } } },
158
+ {
159
+ $project: {
160
+ productId: 1,
161
+ quantity: 1,
162
+ itemAmount: {
163
+ $let: {
164
+ vars: {
165
+ item: {
166
+ $first: {
167
+ $filter: {
168
+ input: '$calculation',
169
+ as: 'c',
170
+ cond: { $eq: ['$$c.category', 'ITEM'] },
171
+ },
172
+ },
173
+ },
174
+ },
175
+ in: '$$item.amount',
176
+ },
177
+ },
178
+ },
179
+ },
180
+ {
181
+ $group: {
182
+ _id: '$productId',
183
+ totalSold: { $sum: '$quantity' },
184
+ totalRevenue: { $sum: '$itemAmount' },
185
+ },
186
+ },
187
+ { $match: { totalSold: { $gt: 0 } } },
188
+ { $sort: { totalSold: -1 } },
189
+ { $limit: limit },
190
+ {
191
+ $project: {
192
+ _id: 0,
193
+ productId: '$_id',
194
+ totalSold: 1,
195
+ totalRevenue: 1,
196
+ },
197
+ },
198
+ ];
199
+ return OrderPositions.aggregate(pipeline, { allowDiskUse: true }).toArray();
200
+ },
154
201
  };
155
202
  };
@@ -28,5 +28,10 @@ export declare const configureOrderModuleMutations: ({ Orders, OrderPositions, }
28
28
  updateContact: (orderId: string, contact: Contact) => Promise<mongodb.WithId<Order> | null>;
29
29
  updateCalculationSheet: (orderId: string, calculation: any) => Promise<mongodb.WithId<Order> | null>;
30
30
  updateContext: (orderId: string, context: any) => Promise<mongodb.WithId<Order> | null>;
31
+ updateCartFields: (orderId: string, updates: {
32
+ meta?: any;
33
+ billingAddress?: Address;
34
+ contact?: Contact;
35
+ }) => Promise<Order | null>;
31
36
  };
32
37
  export type OrderMutations = ReturnType<typeof configureOrderModuleMutations>;
@@ -107,5 +107,26 @@ export const configureOrderModuleMutations = ({ Orders, OrderPositions, }) => {
107
107
  await emit('ORDER_UPDATE', { order, field: 'context' });
108
108
  return order;
109
109
  },
110
+ updateCartFields: async (orderId, updates) => {
111
+ const $set = { updated: new Date() };
112
+ if (updates.billingAddress) {
113
+ $set.billingAddress = updates.billingAddress;
114
+ }
115
+ if (updates.contact) {
116
+ $set.contact = updates.contact;
117
+ }
118
+ if (updates.meta && Object.keys(updates.meta).length > 0) {
119
+ const contextSetters = Object.fromEntries(Object.entries(updates.meta).map(([key, value]) => [`context.${key}`, value]));
120
+ Object.assign($set, contextSetters);
121
+ }
122
+ if (Object.keys($set).length === 1) {
123
+ return Orders.findOne(generateDbFilterById(orderId), {});
124
+ }
125
+ const order = await Orders.findOneAndUpdate(generateDbFilterById(orderId), { $set }, { returnDocument: 'after' });
126
+ if (!order)
127
+ return null;
128
+ await emit('ORDER_UPDATE', { order, field: 'cartFields' });
129
+ return order;
130
+ },
110
131
  };
111
132
  };
@@ -8,13 +8,27 @@ export interface OrderReport {
8
8
  confirmCount: number;
9
9
  fulfillCount: number;
10
10
  }
11
+ export interface TopCustomerRecord {
12
+ userId: string;
13
+ currencyCode: string;
14
+ totalSpent: number;
15
+ orderCount: number;
16
+ lastOrderDate: Date;
17
+ averageOrderValue: number;
18
+ }
11
19
  export interface OrderStatisticsRecord {
12
20
  date: string;
21
+ count: number;
13
22
  total: {
14
23
  amount: number;
15
- currency: string;
24
+ currencyCode: string;
16
25
  };
17
26
  }
27
+ export interface DateRange {
28
+ start?: string;
29
+ end?: string;
30
+ }
31
+ export type StatisticsDateField = 'created' | 'ordered' | 'rejected' | 'confirmed' | 'fullfilled';
18
32
  export interface OrderAggregateParams {
19
33
  match?: Record<string, any>;
20
34
  matchAfterGroup?: Record<string, any>;
@@ -49,5 +63,16 @@ export declare const configureOrdersModuleQueries: ({ Orders }: {
49
63
  orderId: string;
50
64
  }) => Promise<boolean>;
51
65
  aggregateOrders: ({ match, project, group, sort, limit, addFields, pipeline, }: OrderAggregateParams) => Promise<mongodb.Document[]>;
66
+ statistics: {
67
+ countByDateField(dateField: StatisticsDateField, dateRange?: DateRange, options?: {
68
+ includeCarts?: boolean;
69
+ }): Promise<number>;
70
+ aggregateByDateField(dateField: StatisticsDateField, dateRange?: DateRange, options?: {
71
+ includeCarts?: boolean;
72
+ }): Promise<OrderStatisticsRecord[]>;
73
+ getTopCustomers(orderIds: string[], options?: {
74
+ limit?: number;
75
+ }): Promise<TopCustomerRecord[]>;
76
+ };
52
77
  };
53
78
  export type OrderQueries = ReturnType<typeof configureOrdersModuleQueries>;
@@ -2,6 +2,16 @@ import { SortDirection } from '@unchainedshop/utils';
2
2
  import { generateDbFilterById, buildSortOptions, mongodb } from '@unchainedshop/mongodb';
3
3
  import buildFindSelector from "./buildFindSelector.js";
4
4
  import {} from "../db/OrdersCollection.js";
5
+ function buildDateMatch(dateField, dateRange) {
6
+ if (!dateRange?.start && !dateRange?.end)
7
+ return { [dateField]: { $exists: true } };
8
+ const rangeMatch = {};
9
+ if (dateRange?.start)
10
+ rangeMatch.$gte = new Date(dateRange.start);
11
+ if (dateRange?.end)
12
+ rangeMatch.$lte = new Date(dateRange.end);
13
+ return { [dateField]: rangeMatch };
14
+ }
5
15
  export const configureOrdersModuleQueries = ({ Orders }) => {
6
16
  return {
7
17
  isCart: (order) => {
@@ -82,5 +92,119 @@ export const configureOrdersModuleQueries = ({ Orders }) => {
82
92
  stages.push({ $limit: limit });
83
93
  return Orders.aggregate(stages, { allowDiskUse: true }).toArray();
84
94
  },
95
+ statistics: {
96
+ async countByDateField(dateField, dateRange, options) {
97
+ const match = buildDateMatch(dateField, dateRange);
98
+ if (options?.includeCarts) {
99
+ match.status = null;
100
+ match.orderNumber = null;
101
+ }
102
+ const pipeline = [{ $match: match }, { $count: 'count' }];
103
+ const result = await Orders.aggregate(pipeline).toArray();
104
+ return result[0]?.count ?? 0;
105
+ },
106
+ async aggregateByDateField(dateField, dateRange, options) {
107
+ const match = buildDateMatch(dateField, dateRange);
108
+ if (options?.includeCarts) {
109
+ match.status = null;
110
+ match.orderNumber = null;
111
+ }
112
+ const pipeline = [
113
+ { $match: match },
114
+ {
115
+ $addFields: {
116
+ orderTotal: {
117
+ $reduce: {
118
+ input: { $ifNull: ['$calculation', []] },
119
+ initialValue: 0,
120
+ in: { $add: ['$$value', { $ifNull: ['$$this.amount', 0] }] },
121
+ },
122
+ },
123
+ },
124
+ },
125
+ {
126
+ $group: {
127
+ _id: {
128
+ date: { $dateToString: { format: '%Y-%m-%d', date: `$${dateField}` } },
129
+ currency: '$currencyCode',
130
+ },
131
+ totalAmount: { $sum: '$orderTotal' },
132
+ count: { $sum: 1 },
133
+ },
134
+ },
135
+ {
136
+ $project: {
137
+ _id: 0,
138
+ date: '$_id.date',
139
+ total: { amount: '$totalAmount', currencyCode: '$_id.currency' },
140
+ count: 1,
141
+ },
142
+ },
143
+ { $sort: { date: 1 } },
144
+ ];
145
+ return Orders.aggregate(pipeline).toArray();
146
+ },
147
+ async getTopCustomers(orderIds, options) {
148
+ const limit = options?.limit || 10;
149
+ const pipeline = [
150
+ { $match: { _id: { $in: orderIds } } },
151
+ {
152
+ $project: {
153
+ userId: 1,
154
+ created: 1,
155
+ currencyCode: 1,
156
+ itemAmount: {
157
+ $let: {
158
+ vars: {
159
+ item: {
160
+ $first: {
161
+ $filter: {
162
+ input: '$calculation',
163
+ as: 'c',
164
+ cond: { $eq: ['$$c.category', 'ITEMS'] },
165
+ },
166
+ },
167
+ },
168
+ },
169
+ in: '$$item.amount',
170
+ },
171
+ },
172
+ },
173
+ },
174
+ {
175
+ $group: {
176
+ _id: { userId: '$userId', currencyCode: '$currencyCode' },
177
+ totalSpent: { $sum: '$itemAmount' },
178
+ orderCount: { $sum: 1 },
179
+ lastOrderDate: { $max: '$created' },
180
+ },
181
+ },
182
+ { $match: { totalSpent: { $gt: 0 } } },
183
+ {
184
+ $addFields: {
185
+ averageOrderValue: {
186
+ $cond: [{ $eq: ['$orderCount', 0] }, 0, { $divide: ['$totalSpent', '$orderCount'] }],
187
+ },
188
+ currencyCode: '$_id.currencyCode',
189
+ userId: '$_id.userId',
190
+ },
191
+ },
192
+ { $sort: { totalSpent: -1 } },
193
+ { $limit: limit },
194
+ {
195
+ $project: {
196
+ _id: 0,
197
+ userId: 1,
198
+ currencyCode: 1,
199
+ totalSpent: 1,
200
+ orderCount: 1,
201
+ lastOrderDate: 1,
202
+ averageOrderValue: 1,
203
+ },
204
+ },
205
+ ];
206
+ return Orders.aggregate(pipeline, { allowDiskUse: true }).toArray();
207
+ },
208
+ },
85
209
  };
86
210
  };
@@ -76,6 +76,9 @@ export declare const configureOrdersModule: ({ db, migrationRepository, options:
76
76
  }) => Promise<import("../db/OrderPositionsCollection.ts").OrderPosition>;
77
77
  deleteOrderPositions: (orderId: string) => Promise<number>;
78
78
  aggregatePositions: ({ match, project, group, addFields, sort, limit, pipeline, }: import("./configureOrderPositionsModule.ts").OrderPositionAggregateParams) => Promise<mongodb.Document[]>;
79
+ getTopProducts: (orderIds: string[], options?: {
80
+ limit?: number;
81
+ }) => Promise<import("./configureOrderPositionsModule.ts").TopProductRecord[]>;
79
82
  };
80
83
  payments: {
81
84
  findOrderPayment: ({ orderPaymentId, }: {
@@ -133,6 +136,11 @@ export declare const configureOrdersModule: ({ db, migrationRepository, options:
133
136
  updateContact: (orderId: string, contact: import("@unchainedshop/mongodb").Contact) => Promise<mongodb.WithId<Order> | null>;
134
137
  updateCalculationSheet: (orderId: string, calculation: any) => Promise<mongodb.WithId<Order> | null>;
135
138
  updateContext: (orderId: string, context: any) => Promise<mongodb.WithId<Order> | null>;
139
+ updateCartFields: (orderId: string, updates: {
140
+ meta?: any;
141
+ billingAddress?: import("@unchainedshop/mongodb").Address;
142
+ contact?: import("@unchainedshop/mongodb").Contact;
143
+ }) => Promise<Order | null>;
136
144
  isCart: (order: Order) => boolean;
137
145
  cart: ({ orderNumber, countryCode, userId, }: {
138
146
  countryCode?: string;
@@ -154,5 +162,16 @@ export declare const configureOrdersModule: ({ db, migrationRepository, options:
154
162
  orderId: string;
155
163
  }) => Promise<boolean>;
156
164
  aggregateOrders: ({ match, project, group, sort, limit, addFields, pipeline, }: import("./configureOrdersModule-queries.ts").OrderAggregateParams) => Promise<mongodb.Document[]>;
165
+ statistics: {
166
+ countByDateField(dateField: import("./configureOrdersModule-queries.ts").StatisticsDateField, dateRange?: import("./configureOrdersModule-queries.ts").DateRange, options?: {
167
+ includeCarts?: boolean;
168
+ }): Promise<number>;
169
+ aggregateByDateField(dateField: import("./configureOrdersModule-queries.ts").StatisticsDateField, dateRange?: import("./configureOrdersModule-queries.ts").DateRange, options?: {
170
+ includeCarts?: boolean;
171
+ }): Promise<import("./configureOrdersModule-queries.ts").OrderStatisticsRecord[]>;
172
+ getTopCustomers(orderIds: string[], options?: {
173
+ limit?: number;
174
+ }): Promise<import("./configureOrdersModule-queries.ts").TopCustomerRecord[]>;
175
+ };
157
176
  }>;
158
177
  export type OrdersModule = Awaited<ReturnType<typeof configureOrdersModule>>;
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@unchainedshop/core-orders",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "main": "lib/orders-index.js",
5
5
  "types": "lib/orders-index.d.ts",
6
6
  "type": "module",
7
+ "sideEffects": false,
7
8
  "scripts": {
8
9
  "clean": "tsc -b --clean",
9
10
  "build": "tsc -b",
@@ -34,9 +35,9 @@
34
35
  "homepage": "https://github.com/unchainedshop/unchained#readme",
35
36
  "dependencies": {
36
37
  "@kontsedal/locco": "0.1.0",
37
- "@unchainedshop/events": "^4.4.0",
38
- "@unchainedshop/logger": "^4.4.0",
39
- "@unchainedshop/utils": "^4.4.0"
38
+ "@unchainedshop/events": "^4.5.0",
39
+ "@unchainedshop/logger": "^4.5.0",
40
+ "@unchainedshop/utils": "^4.5.0"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@types/node": "^25.0.0",