UNPKG

@rsc-labs/medusa-store-analytics

Version:
507 lines (506 loc) 23.6 kB
"use strict"; /* * Copyright 2024 RSC-Labs, https://rsoftcon.com/ * * MIT License * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); const medusa_1 = require("@medusajs/medusa"); const medusa_2 = require("@medusajs/medusa"); const dateTransformations_1 = require("./utils/dateTransformations"); const typeorm_1 = require("typeorm"); class CustomersAnalyticsService extends medusa_1.TransactionBaseService { constructor(container) { super(container); this.customerService = container.customerService; this.orderService = container.orderService; } async getHistory(from, to, dateRangeFromCompareTo, dateRangeToCompareTo) { if (dateRangeFromCompareTo && from && to && dateRangeToCompareTo) { const resolution = (0, dateTransformations_1.calculateResolution)(from); const customers = await this.activeManager_.getRepository(medusa_2.Customer) .createQueryBuilder('customer') .select(` CASE WHEN customer.created_at < :from AND customer.created_at >= :dateRangeFromCompareTo THEN 'previous' ELSE 'current' END AS type, date_trunc('${resolution}', customer.created_at) AS date `) .setParameters({ from, dateRangeFromCompareTo }) .addSelect('COUNT(customer.id)', 'customerCount') .where(`created_at >= :dateRangeFromCompareTo`, { dateRangeFromCompareTo }) .groupBy('type, date') .orderBy('date, type', 'ASC') .getRawMany(); const finalCustomers = customers.reduce((acc, entry) => { const type = entry.type; const date = entry.date; const customerCount = entry.customerCount; if (!acc[type]) { acc[type] = []; } acc[type].push({ date, customerCount }); return acc; }, {}); return { dateRangeFrom: from.getTime(), dateRangeTo: to.getTime(), dateRangeFromCompareTo: dateRangeFromCompareTo.getTime(), dateRangeToCompareTo: dateRangeToCompareTo.getTime(), current: finalCustomers.current ? finalCustomers.current : [], previous: finalCustomers.previous ? finalCustomers.previous : [], }; } let startQueryFrom; if (!dateRangeFromCompareTo) { if (from) { startQueryFrom = from; } else { // All time const lastCustomer = await this.activeManager_.getRepository(medusa_2.Customer).find({ skip: 0, take: 1, order: { created_at: "ASC" }, }); if (lastCustomer.length > 0) { startQueryFrom = lastCustomer[0].created_at; } } } else { startQueryFrom = dateRangeFromCompareTo; } if (startQueryFrom) { const resolution = (0, dateTransformations_1.calculateResolution)(startQueryFrom); const customers = await this.activeManager_.getRepository(medusa_2.Customer) .createQueryBuilder('customer') .select(`date_trunc('${resolution}', customer.created_at)`, 'date') .addSelect('COUNT(customer.id)', 'customerCount') .where(`created_at >= :startQueryFrom`, { startQueryFrom }) .groupBy('date') .orderBy('date', 'ASC') .getRawMany(); return { dateRangeFrom: startQueryFrom.getTime(), dateRangeTo: to ? to.getTime() : new Date(Date.now()).getTime(), dateRangeFromCompareTo: undefined, dateRangeToCompareTo: undefined, current: customers, previous: [] }; } return { dateRangeFrom: undefined, dateRangeTo: undefined, dateRangeFromCompareTo: undefined, dateRangeToCompareTo: undefined, current: [], previous: [] }; } async getNewCount(from, to, dateRangeFromCompareTo, dateRangeToCompareTo) { let startQueryFrom; if (!dateRangeFromCompareTo) { if (from) { startQueryFrom = from; } else { // All time const lastCustomer = await this.activeManager_.getRepository(medusa_2.Customer).find({ skip: 0, take: 1, order: { created_at: "ASC" } }); if (lastCustomer.length > 0) { startQueryFrom = lastCustomer[0].created_at; } } } else { startQueryFrom = dateRangeFromCompareTo; } const customers = await this.customerService.listAndCount({ created_at: startQueryFrom ? { gte: startQueryFrom } : undefined, }, { select: [ "id", "created_at", "updated_at" ], order: { created_at: "DESC" }, }); if (dateRangeFromCompareTo && from && to && dateRangeToCompareTo) { const previousCustomers = customers[0].filter(customer => customer.created_at < from); const currentCustomers = customers[0].filter(customer => customer.created_at >= from); return { dateRangeFrom: from.getTime(), dateRangeTo: to.getTime(), dateRangeFromCompareTo: dateRangeFromCompareTo.getTime(), dateRangeToCompareTo: dateRangeToCompareTo.getTime(), current: currentCustomers.length, previous: previousCustomers.length }; } if (startQueryFrom && customers.length > 0) { return { dateRangeFrom: startQueryFrom.getTime(), dateRangeTo: to ? to.getTime() : new Date(Date.now()).getTime(), dateRangeFromCompareTo: undefined, dateRangeToCompareTo: undefined, current: customers[1], previous: 0 }; } return { dateRangeFrom: undefined, dateRangeTo: undefined, dateRangeFromCompareTo: undefined, dateRangeToCompareTo: undefined, current: 0, previous: 0 }; } async getNumberOfReturningCustomers() { const customers = await this.customerService.listAndCount({ orders: { gt: 1 } }); return customers[1]; } async getRepeatCustomerRate(orderStatuses, from, to, dateRangeFromCompareTo, dateRangeToCompareTo) { // Use the same query like finding for Orders, but include Customers let startQueryFrom; const orderStatusesAsStrings = Object.values(orderStatuses); if (orderStatusesAsStrings.length) { if (!dateRangeFromCompareTo) { if (from) { startQueryFrom = from; } else { // All time const lastOrder = await this.activeManager_.getRepository(medusa_1.Order).find({ skip: 0, take: 1, order: { created_at: "ASC" }, where: { status: (0, typeorm_1.In)(orderStatusesAsStrings) } }); if (lastOrder.length > 0) { startQueryFrom = lastOrder[0].created_at; } } } else { startQueryFrom = dateRangeFromCompareTo; } const orders = await this.orderService.list({ created_at: startQueryFrom ? { gte: startQueryFrom } : undefined, status: (0, typeorm_1.In)(orderStatusesAsStrings) }, { select: [ "id", "created_at", "updated_at", "customer_id", ], order: { created_at: "DESC" }, }); if (dateRangeFromCompareTo && from && to && dateRangeToCompareTo) { const previousOrders = orders.filter(order => order.created_at < from); const currentOrders = orders.filter(order => order.created_at >= from); const previousOrderCountByCustomer = previousOrders.reduce((acc, order) => { acc[order.customer_id] = (acc[order.customer_id] || 0) + 1; return acc; }, new Map()); const currentOrderCountByCustomer = currentOrders.reduce((acc, order) => { acc[order.customer_id] = (acc[order.customer_id] || 0) + 1; return acc; }, new Map()); const returningCustomersForCurrentOrders = Object.values(currentOrderCountByCustomer).filter(count => parseInt(count) > 1).length; const totalCustomersForCurrentOrders = Object.keys(currentOrderCountByCustomer).length; const returningCustomersForPreviousOrders = Object.values(previousOrderCountByCustomer).filter(count => parseInt(count) > 1).length; const totalCustomersForPreviousOrders = Object.keys(previousOrderCountByCustomer).length; // Return Customer Rate const returnCustomerRateCurrentValue = totalCustomersForCurrentOrders > 0 ? returningCustomersForCurrentOrders * 100 / totalCustomersForCurrentOrders : undefined; const returnCustomerRatePreviousValue = totalCustomersForPreviousOrders > 0 ? returningCustomersForPreviousOrders * 100 / totalCustomersForPreviousOrders : undefined; // Order frequency distribution let currentOneTimeOrders = 0; let currentRepeatOrders = 0; for (const count of Object.values(currentOrderCountByCustomer)) { if (parseInt(count) === 1) { currentOneTimeOrders += count; } else if (parseInt(count) > 1) { currentRepeatOrders += count; } } let previousOneTimeOrders = 0; let previousRepeatOrders = 0; for (const count of Object.values(previousOrderCountByCustomer)) { if (parseInt(count) === 1) { previousOneTimeOrders += count; } else if (parseInt(count) > 1) { previousRepeatOrders += count; } } return { dateRangeFrom: from.getTime(), dateRangeTo: to.getTime(), dateRangeFromCompareTo: dateRangeFromCompareTo.getTime(), dateRangeToCompareTo: dateRangeToCompareTo.getTime(), current: { returnCustomerRate: returnCustomerRateCurrentValue, orderOneTimeFrequency: currentOneTimeOrders * 100 / currentOrders.length, orderRepeatFrequency: currentRepeatOrders * 100 / currentOrders.length }, previous: { returnCustomerRate: returnCustomerRatePreviousValue, orderOneTimeFrequency: previousOneTimeOrders * 100 / previousOrders.length, orderRepeatFrequency: previousRepeatOrders * 100 / previousOrders.length } }; } if (startQueryFrom) { const orderCountByCustomer = orders.reduce((acc, order) => { acc[order.customer_id] = (acc[order.customer_id] || 0) + 1; return acc; }, new Map()); const returningCustomersForCurrentOrders = Object.values(orderCountByCustomer).filter(count => parseInt(count) > 1).length; const totalCustomersForCurrentOrders = Object.keys(orderCountByCustomer).length; // Return Customer Rate const returnCustomerRateCurrentValue = totalCustomersForCurrentOrders > 0 ? returningCustomersForCurrentOrders * 100 / totalCustomersForCurrentOrders : undefined; // Order frequency distribution let currentOneTimeOrders = 0; let currentRepeatOrders = 0; for (const count of Object.values(orderCountByCustomer)) { if (parseInt(count) === 1) { currentOneTimeOrders += count; } else if (parseInt(count) > 1) { currentRepeatOrders += count; } } return { dateRangeFrom: startQueryFrom.getTime(), dateRangeTo: to ? to.getTime() : new Date(Date.now()).getTime(), dateRangeFromCompareTo: undefined, dateRangeToCompareTo: undefined, current: { returnCustomerRate: returnCustomerRateCurrentValue, orderOneTimeFrequency: currentOneTimeOrders * 100 / orders.length, orderRepeatFrequency: currentRepeatOrders * 100 / orders.length }, previous: undefined }; } } return { dateRangeFrom: undefined, dateRangeTo: undefined, dateRangeFromCompareTo: undefined, dateRangeToCompareTo: undefined, current: { returnCustomerRate: 0 }, previous: { returnCustomerRate: 0 } }; } async getCumulativeHistory(from, to, dateRangeFromCompareTo, dateRangeToCompareTo) { if (dateRangeFromCompareTo && from && to && dateRangeToCompareTo) { const resolution = (0, dateTransformations_1.calculateResolution)(from); const afterCustomers = await this.activeManager_.getRepository(medusa_2.Customer) .createQueryBuilder('customer') .select(`date_trunc('${resolution}', customer.created_at) AS date`) .addSelect(`SUM(COUNT(*)) OVER (ORDER BY date_trunc('${resolution}', customer.created_at) ASC) AS cumulative_count`) .where(`date_trunc('${resolution}', customer.created_at) >= :dateRangeFromCompareTo`, { dateRangeFromCompareTo }) .setParameters({ dateRangeFromCompareTo: dateRangeFromCompareTo }) .groupBy('date') .orderBy('date', 'ASC') .getRawMany(); const beforeCustomers = await this.activeManager_.getRepository(medusa_2.Customer) .createQueryBuilder('customer') .select(`COUNT(*) AS cumulative_count`) .where(`customer.created_at < :dateRangeFromCompareTo`, { dateRangeFromCompareTo }) .getRawOne(); // Start from 0 as customer count will be added from beforeCustomers, so first entry will include past count afterCustomers.push({ date: dateRangeFromCompareTo, cumulative_count: '0' }); for (const afterCustomer of afterCustomers) { afterCustomer.cumulative_count = parseInt(afterCustomer.cumulative_count); afterCustomer.cumulative_count += parseInt(beforeCustomers.cumulative_count); } const previousCustomers = afterCustomers.filter(customer => customer.date < from); const currentCustomers = afterCustomers.filter(customer => customer.date >= from); const finalCustomers = { dateRangeFrom: from.getTime(), dateRangeTo: to.getTime(), dateRangeFromCompareTo: dateRangeFromCompareTo.getTime(), dateRangeToCompareTo: dateRangeToCompareTo.getTime(), current: currentCustomers.map(currentCustomer => { return { date: currentCustomer.date, customerCount: currentCustomer.cumulative_count.toString() }; }), previous: previousCustomers.map(previousCustomers => { return { date: previousCustomers.date, customerCount: previousCustomers.cumulative_count.toString() }; }) }; return finalCustomers; } let startQueryFrom; if (!dateRangeFromCompareTo) { if (from) { startQueryFrom = from; } else { // All time const lastCustomer = await this.activeManager_.getRepository(medusa_2.Customer).find({ skip: 0, take: 1, order: { created_at: "ASC" }, }); if (lastCustomer.length > 0) { startQueryFrom = lastCustomer[0].created_at; } } } else { startQueryFrom = dateRangeFromCompareTo; } if (startQueryFrom) { const resolution = (0, dateTransformations_1.calculateResolution)(startQueryFrom); const allCustomers = await this.activeManager_.getRepository(medusa_2.Customer) .createQueryBuilder('customer') .select(`date_trunc('${resolution}', customer.created_at) AS date`) .addSelect(`SUM(COUNT(*)) OVER (ORDER BY date_trunc('${resolution}', customer.created_at) ASC) AS cumulative_count`) .setParameters({ startQueryFrom: startQueryFrom }) .groupBy('date') .orderBy('date', 'ASC') .getRawMany(); const finalCustomers = { dateRangeFrom: startQueryFrom.getTime(), dateRangeTo: to ? to.getTime() : new Date(Date.now()).getTime(), dateRangeFromCompareTo: undefined, dateRangeToCompareTo: undefined, current: allCustomers.map(currentCustomer => { return { date: currentCustomer.date, customerCount: currentCustomer.cumulative_count.toString() }; }), previous: [] }; return finalCustomers; } return { dateRangeFrom: undefined, dateRangeTo: undefined, dateRangeFromCompareTo: undefined, dateRangeToCompareTo: undefined, current: [], previous: [] }; } // Customers which purchased something in the time period / Total customers async getRetentionRate(orderStatuses, from, to, dateRangeFromCompareTo, dateRangeToCompareTo) { // Use the same query like finding for Orders, but include Customers let startQueryFrom; const orderStatusesAsStrings = Object.values(orderStatuses); if (orderStatusesAsStrings.length) { const totalNumberCustomers = await this.customerService.count(); if (!dateRangeFromCompareTo) { if (from) { startQueryFrom = from; } else { // All time const lastOrder = await this.activeManager_.getRepository(medusa_1.Order).find({ skip: 0, take: 1, order: { created_at: "ASC" }, where: { status: (0, typeorm_1.In)(orderStatusesAsStrings) } }); if (lastOrder.length > 0) { startQueryFrom = lastOrder[0].created_at; } } } else { startQueryFrom = dateRangeFromCompareTo; } const orders = await this.orderService.list({ created_at: startQueryFrom ? { gte: startQueryFrom } : undefined, status: (0, typeorm_1.In)(orderStatusesAsStrings) }, { select: [ "id", "created_at", "updated_at", "customer_id", ], order: { created_at: "DESC" }, }); if (dateRangeFromCompareTo && from && to && dateRangeToCompareTo) { const previousOrders = orders.filter(order => order.created_at < from); const currentOrders = orders.filter(order => order.created_at >= from); const previousCustomersSet = previousOrders.reduce((acc, order) => { acc.add(order.customer_id); return acc; }, new Set()); const currentCustomersSet = currentOrders.reduce((acc, order) => { acc.add(order.customer_id); return acc; }, new Set()); const retentionCustomerRatePreviousValue = previousCustomersSet.size * 100 / totalNumberCustomers; const retentionCustomerRateCurrentValue = currentCustomersSet.size * 100 / totalNumberCustomers; return { dateRangeFrom: from.getTime(), dateRangeTo: to.getTime(), dateRangeFromCompareTo: dateRangeFromCompareTo.getTime(), dateRangeToCompareTo: dateRangeToCompareTo.getTime(), current: retentionCustomerRateCurrentValue, previous: retentionCustomerRatePreviousValue }; } if (startQueryFrom) { const currentCustomersSet = orders.reduce((acc, order) => { acc.add(order.customer_id); return acc; }, new Set()); const retentionCustomerRateCurrentValue = currentCustomersSet.size * 100 / totalNumberCustomers; return { dateRangeFrom: startQueryFrom.getTime(), dateRangeTo: to ? to.getTime() : new Date(Date.now()).getTime(), dateRangeFromCompareTo: undefined, dateRangeToCompareTo: undefined, current: retentionCustomerRateCurrentValue, previous: undefined }; } } return { dateRangeFrom: undefined, dateRangeTo: undefined, dateRangeFromCompareTo: undefined, dateRangeToCompareTo: undefined, current: undefined, previous: undefined }; } } exports.default = CustomersAnalyticsService;