@nimee/inforu 1.0.17 → 1.0.19

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.
@@ -0,0 +1,346 @@
1
+ import { CustomError } from "@nimee/error-handler";
2
+ import logger from "@nimee/logger";
3
+ import axios from "axios";
4
+
5
+ // Type definitions for ecommerce order
6
+ export interface IProductAttribute {
7
+ label: string;
8
+ value: string;
9
+ }
10
+
11
+ export interface IOrderItem {
12
+ ProductCode: string;
13
+ ProductName: string;
14
+ ProductPrice: number;
15
+ ProductQty: number;
16
+ ProductDescription?: string | null;
17
+ ProductLink?: string;
18
+ ProductImage?: string;
19
+ Custom1?: string;
20
+ Custom2?: string;
21
+ Custom3?: string;
22
+ Custom4?: string;
23
+ Custom5?: string;
24
+ ProductAttributes?: IProductAttribute[];
25
+ }
26
+
27
+ export interface IEcommerceOrderData {
28
+ Event: string;
29
+ Category?: string; // Original event type (e.g., PURCHASED_PHYSICAL_BOOK)
30
+ StoreName: string;
31
+ StoreBaseUrl: string;
32
+ LinkToCart?: string;
33
+ IP?: string;
34
+ CustomerEmail: string;
35
+ CustomerFirstName: string;
36
+ CustomerLastName: string;
37
+ ContactRefId?: string;
38
+ SubscriberStatus?: string;
39
+ AddToGroupName?: string[];
40
+ OrderNumber: string;
41
+ OrderAmount: string | number;
42
+ OrderStatus: string;
43
+ BillingAddress?: string;
44
+ ShippingAddress?: string;
45
+ PaymentDescription?: string;
46
+ ShippingDescription?: string;
47
+ OrderTime: string;
48
+ OrderItems: IOrderItem[];
49
+ }
50
+
51
+ export interface IEcommercePayload {
52
+ Data: IEcommerceOrderData;
53
+ }
54
+
55
+ // Type for ecommerce events
56
+ export enum EcommerceEventType {
57
+ // Purchase events
58
+ PURCHASED_PHYSICAL_BOOK = "PURCHASED_PHYSICAL_BOOK",
59
+ PURCHASED_DIGITAL_BOOK = "PURCHASED_DIGITAL_BOOK",
60
+ PURCHASED_VOD_PRODUCT = "PURCHASED_VOD_PRODUCT",
61
+ PURCHASED_EVENT_TICKET = "PURCHASED_EVENT_TICKET",
62
+
63
+ // Abandoned cart events
64
+ ABANDONED_CART_BOOK = "ABANDONED_CART_BOOK",
65
+ ABANDONED_CART = "ABANDONED_CART", // Generic abandoned cart
66
+ }
67
+
68
+ // Map internal event types to InfoRu API event names
69
+ const EVENT_TYPE_TO_API_EVENT: Record<EcommerceEventType, string> = {
70
+ [EcommerceEventType.PURCHASED_PHYSICAL_BOOK]: "sales_order_place_after",
71
+ [EcommerceEventType.PURCHASED_DIGITAL_BOOK]: "sales_order_place_after",
72
+ [EcommerceEventType.PURCHASED_VOD_PRODUCT]: "sales_order_place_after",
73
+ [EcommerceEventType.PURCHASED_EVENT_TICKET]: "sales_order_place_after",
74
+ [EcommerceEventType.ABANDONED_CART_BOOK]: "cart_abandoned",
75
+ [EcommerceEventType.ABANDONED_CART]: "cart_abandoned",
76
+ };
77
+
78
+ // Interface for product/item in order
79
+ export interface IOrderProduct {
80
+ code: string;
81
+ name: string;
82
+ price: number;
83
+ quantity: number;
84
+ description?: string;
85
+ link?: string;
86
+ image?: string;
87
+ // Book-specific fields
88
+ isbn?: string;
89
+ author?: string;
90
+ publisher?: string;
91
+ format?: string; // "physical" or "digital"
92
+ downloadLink?: string; // for digital products
93
+ // Event-specific fields
94
+ eventDate?: string | Date;
95
+ eventLocation?: string;
96
+ seatNumber?: string;
97
+ // VOD-specific fields
98
+ vodDuration?: string;
99
+ vodExpiry?: string | Date;
100
+ vodStreamUrl?: string;
101
+ }
102
+
103
+ // Interface for ecommerce purchase input data
104
+ export interface IEcommercePurchaseData {
105
+ customerEmail: string;
106
+ customerFirstName: string;
107
+ customerLastName: string;
108
+ orderNumber: string;
109
+ orderAmount: number | string;
110
+ orderStatus?: string;
111
+ billingAddress?: string;
112
+ shippingAddress?: string;
113
+ paymentMethod?: string;
114
+ shippingMethod?: string;
115
+ orderTime?: string | Date;
116
+ products: IOrderProduct[];
117
+ storeName?: string;
118
+ storeUrl?: string;
119
+ ip?: string;
120
+ contactRefId?: string;
121
+ // Additional fields for abandoned carts
122
+ cartUrl?: string;
123
+ abandonedAt?: string | Date;
124
+ reminderCount?: number;
125
+ }
126
+
127
+ /**
128
+ * Helper function to determine product type from event type
129
+ */
130
+ function getProductType(eventType: EcommerceEventType): string {
131
+ switch (eventType) {
132
+ case EcommerceEventType.PURCHASED_PHYSICAL_BOOK:
133
+ return "physical_book";
134
+ case EcommerceEventType.PURCHASED_DIGITAL_BOOK:
135
+ return "digital_book";
136
+ case EcommerceEventType.PURCHASED_VOD_PRODUCT:
137
+ return "vod";
138
+ case EcommerceEventType.PURCHASED_EVENT_TICKET:
139
+ return "event_ticket";
140
+ case EcommerceEventType.ABANDONED_CART_BOOK:
141
+ return "abandoned_book";
142
+ case EcommerceEventType.ABANDONED_CART:
143
+ return "abandoned_cart";
144
+ default:
145
+ return "product";
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Transforms purchase data into ecommerce API format
151
+ */
152
+ function prepareEcommercePayload(
153
+ eventType: EcommerceEventType,
154
+ data: IEcommercePurchaseData
155
+ ): IEcommercePayload {
156
+ const isAbandonedCart = eventType === EcommerceEventType.ABANDONED_CART_BOOK || eventType === EcommerceEventType.ABANDONED_CART;
157
+
158
+ // Map products to order items
159
+ const orderItems: IOrderItem[] = data.products.map((product: IOrderProduct) => ({
160
+ ProductCode: product.code || product.isbn || "",
161
+ ProductName: product.name,
162
+ ProductPrice: product.price,
163
+ ProductQty: product.quantity,
164
+ ProductDescription: product.description || null,
165
+ ProductLink: product.link,
166
+ ProductImage: product.image,
167
+ // Custom fields based on product type
168
+ Custom1: product.isbn || product.eventDate?.toString() || product.vodDuration,
169
+ Custom2: product.author || product.eventLocation || product.vodExpiry?.toString(),
170
+ Custom3: product.publisher || product.seatNumber || product.vodStreamUrl,
171
+ Custom4: product.format || getProductType(eventType),
172
+ Custom5: eventType, // Add the original event type (e.g., PURCHASED_PHYSICAL_BOOK)
173
+ ProductAttributes: [
174
+ // Always include the event category
175
+ { label: "event_category", value: eventType },
176
+ ...(product.format ? [{ label: "format", value: product.format }] : []),
177
+ ...(product.author ? [{ label: "author", value: product.author }] : []),
178
+ ...(product.publisher ? [{ label: "publisher", value: product.publisher }] : []),
179
+ ...(product.eventDate ? [{ label: "event_date", value: product.eventDate.toString() }] : []),
180
+ ...(product.eventLocation ? [{ label: "location", value: product.eventLocation }] : []),
181
+ ...(product.seatNumber ? [{ label: "seat", value: product.seatNumber }] : []),
182
+ ...(product.downloadLink ? [{ label: "download_link", value: product.downloadLink }] : []),
183
+ ].filter(attr => attr.value) // Remove empty attributes
184
+ }));
185
+
186
+ // Format order time
187
+ const orderTime = data.orderTime
188
+ ? (data.orderTime instanceof Date
189
+ ? data.orderTime.toISOString().replace('T', ' ').substring(0, 19)
190
+ : data.orderTime)
191
+ : new Date().toISOString().replace('T', ' ').substring(0, 19);
192
+
193
+ // Get the appropriate InfoRu API event name
194
+ const apiEventName = EVENT_TYPE_TO_API_EVENT[eventType];
195
+
196
+ // Determine group names based on event type
197
+ const groupNames: string[] = [];
198
+ switch (eventType) {
199
+ case EcommerceEventType.PURCHASED_PHYSICAL_BOOK:
200
+ groupNames.push("physical_book_buyers");
201
+ break;
202
+ case EcommerceEventType.PURCHASED_DIGITAL_BOOK:
203
+ groupNames.push("digital_book_buyers");
204
+ break;
205
+ case EcommerceEventType.PURCHASED_VOD_PRODUCT:
206
+ groupNames.push("vod_purchasers");
207
+ break;
208
+ case EcommerceEventType.PURCHASED_EVENT_TICKET:
209
+ groupNames.push("event_attendees");
210
+ break;
211
+ case EcommerceEventType.ABANDONED_CART_BOOK:
212
+ groupNames.push("abandoned_cart_books");
213
+ break;
214
+ case EcommerceEventType.ABANDONED_CART:
215
+ groupNames.push("abandoned_cart_general");
216
+ break;
217
+ }
218
+
219
+ // Determine if physical shipping is needed
220
+ const needsShipping = eventType === EcommerceEventType.PURCHASED_PHYSICAL_BOOK ||
221
+ eventType === EcommerceEventType.PURCHASED_EVENT_TICKET;
222
+
223
+ return {
224
+ Data: {
225
+ Event: apiEventName,
226
+ Category: eventType, // Add the original event type (e.g., PURCHASED_PHYSICAL_BOOK)
227
+ StoreName: data.storeName || "Nimi Store",
228
+ StoreBaseUrl: data.storeUrl || "https://www.nimi.co.il/",
229
+ LinkToCart: isAbandonedCart && data.cartUrl
230
+ ? data.cartUrl
231
+ : `${data.storeUrl || "https://www.nimi.co.il"}/checkout/cart/`,
232
+ IP: data.ip,
233
+ CustomerEmail: data.customerEmail,
234
+ CustomerFirstName: data.customerFirstName,
235
+ CustomerLastName: data.customerLastName,
236
+ ContactRefId: data.contactRefId || "",
237
+ SubscriberStatus: "Subscribed",
238
+ AddToGroupName: groupNames,
239
+ OrderNumber: data.orderNumber,
240
+ OrderAmount: String(data.orderAmount),
241
+ OrderStatus: isAbandonedCart ? "abandoned" : (data.orderStatus || "processing"),
242
+ BillingAddress: data.billingAddress,
243
+ ShippingAddress: needsShipping ? data.shippingAddress : undefined,
244
+ PaymentDescription: data.paymentMethod || "credit card",
245
+ ShippingDescription: needsShipping
246
+ ? (data.shippingMethod || "Standard shipping")
247
+ : "Digital delivery - Instant",
248
+ OrderTime: orderTime,
249
+ OrderItems: orderItems
250
+ }
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Sends ecommerce order data to InfoRu API based on event type
256
+ */
257
+ export async function sendEcommerceOrder(params: {
258
+ apiKey: string;
259
+ eventType: EcommerceEventType;
260
+ data: IEcommercePurchaseData;
261
+ }) {
262
+ try {
263
+ const { apiKey, eventType, data } = params;
264
+
265
+ if (!apiKey) {
266
+ throw new CustomError("MISSING_API_KEY", 400, "API key is required");
267
+ }
268
+
269
+ if (!eventType || !Object.values(EcommerceEventType).includes(eventType)) {
270
+ throw new CustomError("INVALID_EVENT_TYPE", 400, `Invalid event type. Must be one of: ${Object.values(EcommerceEventType).join(', ')}`);
271
+ }
272
+
273
+ if (!data || !data.customerEmail || !data.orderNumber) {
274
+ throw new CustomError("MISSING_REQUIRED_DATA", 400, "Customer email and order number are required");
275
+ }
276
+
277
+ if (!data.products || data.products.length === 0) {
278
+ throw new CustomError("NO_ORDER_ITEMS", 400, "Order must contain at least one product");
279
+ }
280
+
281
+ // Prepare the payload based on event type
282
+ const payload = prepareEcommercePayload(eventType, data);
283
+
284
+ // API endpoint for ecommerce
285
+ const apiUrl = "https://capi.inforu.co.il/api/v2/EcommerceApi";
286
+
287
+ // Send the request
288
+ const response = await axios.post(apiUrl, payload, {
289
+ headers: {
290
+ "Authorization": apiKey,
291
+ "Content-Type": "application/json"
292
+ }
293
+ });
294
+
295
+ return {
296
+ success: true,
297
+ data: response.data,
298
+ orderNumber: data.orderNumber,
299
+ eventType
300
+ };
301
+
302
+ } catch (error) {
303
+ logger.error("Failed to send ecommerce order", {
304
+ error: error instanceof Error ? error.message : error,
305
+ eventType: params.eventType,
306
+ orderNumber: params.data?.orderNumber
307
+ });
308
+
309
+ if (axios.isAxiosError(error)) {
310
+ throw new CustomError(
311
+ "ECOMMERCE_API_ERROR",
312
+ error.response?.status || 500,
313
+ `Failed to send ecommerce order: ${error.response?.data?.message || error.message}`
314
+ );
315
+ }
316
+
317
+ throw new CustomError(
318
+ "ECOMMERCE_ORDER_ERROR",
319
+ 500,
320
+ `Failed to process ecommerce order: ${error instanceof Error ? error.message : JSON.stringify(error)}`
321
+ );
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Helper function to determine if an event is an ecommerce event
327
+ */
328
+ export function isEcommerceEvent(eventName: string): boolean {
329
+ return Object.values(EcommerceEventType).includes(eventName as EcommerceEventType);
330
+ }
331
+
332
+ /**
333
+ * Helper function to validate and parse ecommerce event type
334
+ */
335
+ export function parseEcommerceEventType(eventName: string): EcommerceEventType | null {
336
+ if (Object.values(EcommerceEventType).includes(eventName as EcommerceEventType)) {
337
+ return eventName as EcommerceEventType;
338
+ }
339
+ return null;
340
+ }
341
+
342
+ // Backward compatibility aliases for book events
343
+ export const BookPurchaseEventType = EcommerceEventType; // Type alias
344
+ export type IBookPurchaseData = IEcommercePurchaseData; // Interface alias with books instead of products
345
+ export const isBookPurchaseEvent = isEcommerceEvent;
346
+ export const parseBookPurchaseEventType = parseEcommerceEventType;
package/src/index.ts CHANGED
@@ -4,3 +4,4 @@ export * from "./createGroup";
4
4
  export * from "./automations";
5
5
  export * from "./sendWA";
6
6
  export * from "./sendEmail";
7
+ export * from "./ecommerce";
package/src/normalize.ts CHANGED
@@ -12,11 +12,11 @@ function normalizeBetweenEndUserToContacts(contacts: IEndUserModel[], groupName?
12
12
  ParticipationAtEventsCounter: (contact as any).participationAtEventsCounter,
13
13
  AddToGroupName: groupName,
14
14
  ContactRefId: (contact as any)._id,
15
- OrderId: (contact as any).Order_id,
15
+ Order_id: (contact as any).Order_id,
16
16
  Status: (contact as any).Status,
17
17
  Total: (contact as any).Total,
18
18
  Currency: (contact as any).Currency,
19
- PaymentMethod: (contact as any).Payment_method,
19
+ Payment_method: (contact as any).Payment_method,
20
20
  Product1Name: (contact as any).Products?.[0]?.Name,
21
21
  Product1Price: (contact as any).Products?.[0]?.Price,
22
22
  Product1Quantity: (contact as any).Products?.[0]?.Quantity,
@@ -37,28 +37,6 @@ function normalizeBetweenEndUserToContacts(contacts: IEndUserModel[], groupName?
37
37
  UpdatedAt: (contact as any).updatedAt,
38
38
  GettingKnowRadical: (contact as any).GettingKnowRadical,
39
39
  ActionName: (contact as any).actionName,
40
- EventName: (contact as any).eventName,
41
- EventStartDate: (contact as any).eventStartDate,
42
- EventStartHour: (contact as any).eventStartHour,
43
- Ticket1Name: (contact as any).tickets?.[0]?.ticketName,
44
- Ticket1Quantity: (contact as any).tickets?.[0]?.quantity,
45
- Ticket2Name: (contact as any).tickets?.[1]?.ticketName,
46
- Ticket2Quantity: (contact as any).tickets?.[1]?.quantity,
47
- Ticket3Name: (contact as any).tickets?.[2]?.ticketName,
48
- Ticket3Quantity: (contact as any).tickets?.[2]?.quantity,
49
- Ticket4Name: (contact as any).tickets?.[3]?.ticketName,
50
- Ticket4Quantity: (contact as any).tickets?.[3]?.quantity,
51
- Ticket5Name: (contact as any).tickets?.[4]?.ticketName,
52
- Ticket5Quantity: (contact as any).tickets?.[4]?.quantity,
53
- Ticket6Name: (contact as any).tickets?.[5]?.ticketName,
54
- Ticket6Quantity: (contact as any).tickets?.[5]?.quantity,
55
- Ticket7Name: (contact as any).tickets?.[6]?.ticketName,
56
- Ticket7Quantity: (contact as any).tickets?.[6]?.quantity,
57
- SubscriptionName: (contact as any).subscriptionName,
58
- SubscriptionPrice: (contact as any).subscriptionPrice,
59
- OrderNumber: (contact as any).orderNumber,
60
- TicketsAmount: (contact as any).ticketsAmount,
61
- TicketsLink: (contact as any).ticketsLink,
62
40
  };
63
41
  });
64
42
  }