@nimee/inforu 1.0.16 → 1.0.18
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/dist/automations.js +89 -1
- package/dist/automations.js.map +1 -1
- package/dist/ecommerce-example.d.ts +10 -0
- package/dist/ecommerce-example.js +246 -0
- package/dist/ecommerce-example.js.map +1 -0
- package/dist/ecommerce.d.ts +119 -0
- package/dist/ecommerce.js +232 -0
- package/dist/ecommerce.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/automations.ts +108 -1
- package/src/ecommerce-curl-examples.md +361 -0
- package/src/ecommerce.ts +346 -0
- package/src/index.ts +1 -0
package/src/ecommerce.ts
ADDED
|
@@ -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