@apihawk/billia-sdk
Version:
The ApiHawk Billia SDK
443 lines (442 loc) • 17 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const billia_sdk_service_base_1 = require("../lib/billia-sdk-service-base");
const query_builder_1 = require("./common/query-builder");
const to_rest_resource_1 = require("./common/to-rest-resource");
const ORDER_DETAILS_VISIBILITY = 'customer-portal__order-details';
const PENDING_STATUSES = [
'approved',
'processing',
'in-progress',
'provisioned',
'collapsed'
];
class BilliaSDKOrders extends billia_sdk_service_base_1.BilliaSDKServiceBase {
constructor(api, paymentService, catalogService) {
super(api);
this.paymentService = paymentService;
this.catalogService = catalogService;
}
/**
* Lists purchases made by the user. Each purchase contains the nested order lines.
*
* @param session user session
* @param query pagination query
* @param options additional options
*/
getOrders(session, query = {}, options = {
paymentMethods: false // whether to fetch payment methods for each purchase
}) {
return __awaiter(this, void 0, void 0, function* () {
// decide if payment methods should be fetched
const fetchPaymentMethods = options.paymentMethods
? () => this.paymentService.getPaymentMethods(session, {
images: options && options.paymentMethods
})
: () => Promise.resolve([]);
const q = query_builder_1.default.parse(query);
// check if there is a filter by order status
// and if the order status is 'pending',
// send all 'pending' statuses automatically
if (Array.isArray(q.where)) {
const statusFilter = q.where.find((f) => f.field === 'order.status');
if (statusFilter && statusFilter.values.indexOf('pending') >= 0) {
statusFilter.values = [...statusFilter.values, ...PENDING_STATUSES];
}
}
const [ordersResponse, paymentMethods] = yield Promise.all([
this.api.call({
url: '/database/order-purchase',
method: 'GET',
session,
query: q,
headers: {
Accept: 'application/hal+json',
'Accept-Response': 'Advanced'
}
}),
fetchPaymentMethods()
]);
const lang = ordersResponse._language.current_language;
const ordersList = to_rest_resource_1.toRestResource(ordersResponse, 'order_purchase');
// attach computed properties
ordersList.items = ordersList.items.map((item) => {
const status = this.calculateOrderStatus(item);
const type = this.calculateOrderType(item);
const paid = this.calculatePaymentStatus(item);
const paymentMethod = paymentMethods.find((method) => method.id === item.payment_method_id);
const detailed = Object.assign({}, item, { status,
type,
paid, notes: [], overview: this.getOrderOverview(item.order, lang), payment_method: paymentMethod });
return detailed;
});
return ordersList;
});
}
/**
* Fetches an order (purchase) by its ID.
*
* @param session user session
* @param orderId the ID of the order
*/
getOrderById(session, orderId) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield this.api.call({
url: `/database/order-purchase/${orderId}`,
method: 'GET',
session,
headers: {
Accept: 'application/hal+json',
'Accept-Response': 'Advanced'
}
});
const orderLine = response.order[0];
const discountCode = orderLine ? orderLine.discount_code : undefined;
const order = response;
const uniqueCategoryIds = Array.from(new Set(order.order
.filter((o) => !Array.isArray(o.product))
.map((o) => {
const p = o.product;
return p.category_id;
})));
const [notes, paymentMethod, categories] = yield Promise.all([
this.getOrderNotes(session, orderId),
this.paymentService
.getPaymentMethodById(session, order.payment_method_id)
.catch((error) => {
if (error.status === 404) {
return undefined;
}
throw error;
}),
this.catalogService.getAllCategories(uniqueCategoryIds)
]);
const categoryByIdMap = new Map();
if (categories) {
categories.items.forEach((c) => {
categoryByIdMap.set(c.id, c);
});
}
// add the calculated order metadata
order.discount_code = discountCode;
order.status = this.calculateOrderStatus(response);
order.type = this.calculateOrderType(response);
order.paid = this.calculatePaymentStatus(response);
if (notes) {
order.notes = notes;
}
order.payment_method = paymentMethod;
// attach product category to each order line
order.order = order.order.map((o) => {
if (Array.isArray(o.product) && o.product.length === 0) {
return o;
}
const p = o.product;
p.category = categoryByIdMap.get(p.category_id);
o.product = p;
return o;
});
let totalTax = 0;
order.order.map((item) => {
totalTax += +item.price.tax.amount;
});
order.total_amount.tax = {
amount: +totalTax,
percent: +order.order[0].price.tax.percent
};
return order;
});
}
/**
* Cancels a whole purchase which may consist of multiple order lines (products).
*
* @param session user session
* @param orderId the ID of the order line
*/
cancelOrder(session, orderId) {
return __awaiter(this, void 0, void 0, function* () {
const order = yield this.getOrderById(session, orderId);
const orderLines = order.order;
return Promise.all(orderLines.map((ol) => this.cancelOrderLine(session, ol.order_id)));
});
}
/**
* Cancels the purchase of a single order line (product).
*
* @param session user session
* @param orderLineId the ID of the order line
*/
cancelOrderLine(session, orderLineId) {
return this.api.call({
url: `/database/order/${orderLineId}`,
method: 'PATCH',
session,
body: {
status: 'canceled'
}
});
}
/**
* Gets detailed information about the order lines (product options) of a order.
*
* @param session user session
* @param orderId the order ID
*/
getOrderLines(session, orderId) {
return __awaiter(this, void 0, void 0, function* () {
const filter = {
where: [
{
field: 'purchase_id',
where: 'and',
type: 'equalTo',
value: orderId
}
]
};
const response = yield this.api.call({
url: '/database/order?' + filter,
method: 'GET',
session,
headers: {
'Accept-Response': 'Advanced'
}
});
return this._transformOrderLines(response);
});
}
/**
* Fetches a list of purchases which contain unpaid order lines.
*
* @param session - user session
*/
getUnpaidPurchases(session) {
return __awaiter(this, void 0, void 0, function* () {
const filter = {
page_size: -1,
where: [
{
field: 'paid',
where: 'and',
type: 'equalTo',
value: 0
},
{
field: 'status',
where: 'and',
type: 'notEqualTo',
value: 'canceled'
}
]
};
const ordersResource = yield this.api.call({
url: '/database/order',
method: 'GET',
query: filter,
session
});
const unpaidPurchaseIds = Array.from(new Set(ordersResource._embedded.order.map((ol) => ol.purchase_id)));
// if there are no unpaid purchases, exit early
if (unpaidPurchaseIds.length === 0) {
return {
items: [],
page: 1,
page_count: 1,
page_size: 25,
total_items: 0
};
}
const purchasesResource = yield this.getOrders(session, {
filter: {
purchase_id: unpaidPurchaseIds.map((x) => `${x}`)
}
}, { paymentMethods: true });
const lang = purchasesResource._language
? purchasesResource._language.current_language
: 'en-US';
const purchases = purchasesResource.items.map((purchase) => {
purchase.currency = purchase.order[0]
? purchase.order[0].price._currency.currency
: '';
purchase.overview = this.getOrderOverview(purchase.order, lang) || '';
return purchase;
});
// add currency data to the total
return Object.assign({}, purchasesResource, {
items: purchases
});
});
}
/**
* Lists all the notes associated with a purchase.
* @param session user session
* @param orderId order ID
*/
getOrderNotes(session, orderId) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield this.api.call({
url: `/database/order-purchase/${orderId}/notes?page_size=-1`,
method: 'GET',
session
});
const notes = response._embedded.notes;
return notes;
});
}
/**
* Flattens bundle items which are part of the purchase's ordered items.
*/
_transformOrderLines(orderLines) {
const purchasedItems = orderLines._embedded.order || [];
const purchasedItemsCount = purchasedItems.length;
let nonBundles = [];
let bundles = [];
for (let i = 0; i < purchasedItemsCount; ++i) {
if (purchasedItems[i].product_type === 'bundle') {
bundles.push(purchasedItems[i]);
}
else {
nonBundles.push(purchasedItems[i]);
}
}
bundles = bundles.map((bundle) => {
bundle.bundle_items = nonBundles.filter((i) => i.parent_id === bundle.order_id);
// calculates the total price of the bundle
bundle.price = bundle.bundle_items.reduce((accumulated, current) => {
accumulated.total.amount += +current.price.total.amount || 0;
accumulated.tax.amount += +current.price.tax.amount || 0;
return accumulated;
}, bundle.price);
return bundle;
});
nonBundles = nonBundles.filter((i) => !i.parent_id);
const orders = bundles
.concat(nonBundles)
.map((orderLine) => {
orderLine.options = (orderLine.options || []).filter((o) => (o.option.visibility || []).find((v) => v === ORDER_DETAILS_VISIBILITY));
return orderLine;
})
.sort((a, b) => a.order_id - b.order_id);
return orders;
}
getOrderOverview(orders, lang = 'en-US') {
const DOMAIN_PROVIDERS = [
'Ficora',
'Ascio',
'ResellerClub',
'RegistryEu',
'RegistryBg'
];
let sharedHostingDomain = null;
const firstTwoProducts = [];
let currentProduct = '';
let i = 0;
let len = 0;
if (orders[0] && orders[0].order_type === 'fund') {
return 'Adding of funds';
}
for (i = 0, len = orders.length; i < len; ++i) {
currentProduct = '';
// if there are more than two products, just add elipsis
if (i > 1) {
return firstTwoProducts.join(', ') + ', ...';
}
// when the type of the order is "add funds",
// the product property is an empty array
if (Array.isArray(orders[i].product)) {
return 'Adding of funds';
}
const product = orders[i].product;
if (product._language.translations.name) {
currentProduct = product._language.translations.name[lang].value;
}
// if the product is a domain - use the product descriptor instead of the name
if (DOMAIN_PROVIDERS.indexOf(product.module) > -1) {
currentProduct = orders[i].descriptor;
}
else if (product.module === 'Cpanel') {
// show the domain name associated to the shared hosting plan
if (orders[i].options) {
sharedHostingDomain = (orders[i].options || []).find((option) => option.option.option_key === 'domain' ||
option.option.option_key === 'DomainName' ||
option.option.option_key === 'domain_name');
}
currentProduct = sharedHostingDomain
? currentProduct + ' (' + sharedHostingDomain.value + ')'
: currentProduct;
}
firstTwoProducts.push(currentProduct);
}
return firstTwoProducts.join(', ');
}
/**
* Calculates the type of an order
* by iterating over the types of its nested order lines.
*
* @param order - order
*/
calculateOrderType(order) {
const orders = order.order;
let i = 0;
let len = 0;
const typesMap = {};
let typesArray;
for (i = 0, len = orders.length; i < len; i++) {
typesMap[orders[i].order_type] = true;
}
typesArray = Object.keys(typesMap);
if (typesArray.length > 1) {
return 'mixed';
}
else {
return typesArray[0] || 'unknown';
}
}
/**
* Calculates the status of an order
* by iterating over the statuses of its nested order lines.
*
* @param order - order record
*/
calculateOrderStatus(order) {
const orders = order.order;
if (orders.length === 0) {
return 'unknown';
}
for (let i = 0, len = orders.length; i < len - 1; ++i) {
if (orders[i].status !== orders[i + 1].status) {
return 'mixed';
}
}
return orders[0].status ? orders[0].status : 'unknown';
}
/**
* Calculates the payment status of an order
* by iterating over the statuses of its nested order lines.
*
* @param order - order details
*/
calculatePaymentStatus(order) {
const paidOrders = order.order.filter((o) => {
return !!o.paid;
});
if (paidOrders.length === 0) {
return 'no';
}
else if (paidOrders.length === order.order.length) {
return 'yes';
}
else {
return 'partially';
}
}
}
exports.BilliaSDKOrders = BilliaSDKOrders;