'use strict';
const BaseRepository = require('../common/BaseRepository.js');
const TransactionEntity = require('./TransactionEntity.js');
const PaymentTypeEntity = require('./payment-types/PaymentTypeEntity.js');
const TransactionTypeEntity = require('./transaction-types/TransactionTypeEntity.js');
const TransactionCategoryEntity = require('./transaction-categories/TransactionCategoryEntity.js');
const enums = require('../utils/enums.js');
/**
* @module Transactions Domain
* @fileoverview TransactionRepository - Manages transactions data.
* @class TransactionRepository
*/
class TransactionRepository extends BaseRepository {
/**
* @constructor
* @param {Object} DB
* @returns {TransactionRepository}
*/
constructor({ DB }) {
super({
DB : DB || require('@vectoricons.net/db'),
modelName: 'transactions',
entityClass: TransactionEntity,
});
}
/**
* Get a transaction by ID and fetch related data.
* @param {Number} transactionId - The transaction ID.
* @returns {Object} - The transaction object.
* @throws {Error} - If an error occurs.
*/
async getTransactionWithItems(transactionId) {
console.log('TransactionRepository.getTransactionWithItems transactionId', transactionId);
const transaction = await this.Table.query()
.findById(transactionId)
.withGraphFetched('[transactionItems, couponCode, discounts]');
const result = await this.DB.knex.raw(`
SELECT json_build_object(
'transaction_id', dte_transaction.entity_id,
'original_price', dte_transaction.original_price,
'discounted_price', dte_transaction.discounted_price,
'discount_amount', dte_transaction.discount_amount,
'discount_type', dte_transaction.discount_type,
'items', COALESCE(json_agg(json_build_object(
'transaction_item_id', dte_items.entity_id,
'original_price', dte_items.original_price,
'discounted_price', dte_items.discounted_price,
'discount_amount', dte_items.discount_amount,
'discount_type', dte_items.discount_type
)) FILTER (WHERE dte_items.id IS NOT NULL), '[]')
) AS discount_tree
FROM discounts_to_entities dte_transaction
LEFT JOIN discounts_to_entities dte_items
ON dte_items.parent_id = dte_transaction.id
WHERE dte_transaction.entity_type = 'transaction'
AND dte_transaction.entity_id = ${transactionId}
GROUP BY dte_transaction.id;
`);
transaction.discounts = result?.rows[0]?.discount_tree || null;
return transaction;
}
/**
* Get a transaction by user ID.
* @param {Number} userId - The user ID.
* @returns {Object} - The transaction object.
* @throws {Error} - If an error occurs.
*/
async getTransactionByUserId(userId) {
const transaction = await this.Table.query().findOne({
user_id: userId,
status: enums.transactionStatus.NotProcessed,
});
return this.getTransactionWithItems(transaction.id);
}
/**
* Update the totals of a transaction.
* @param {Number} transactionId - The transaction ID.
* @returns {Number} - The transaction total.
* @throws {Error} - If an error occurs.
*/
async updateTotals(transactionId) {
const transactionTotals = await this.DB.transactionTotalsView.query().findById(transactionId);
const transactionTotal = Number(transactionTotals?.total_price || 0);
await this.Table.query()
.findById(transactionId)
.patch({
subtotal: Number(transactionTotal.toFixed(2)),
total: Number(transactionTotal.toFixed(2)),
});
return transactionTotal;
}
/**
* Get discounts for a transaction item.
* @param {Number} transactionItemId - The transaction item ID.
* @returns {Object} - The discounts object.
* @throws {Error} - If an error occurs.
*/
async insertDiscount(data) {
return this.DB.discountsToEntities.query().insert(data).returning('*');
}
/**
* Upsert a discount for a transaction or transaction item.
* @param {Object} data - The discount data.
* @param {string} data.entity_type - The entity type (transaction or transaction_item).
* @param {number} data.entity_id - The entity ID.
* @param {number} data.coupon_code_id - The coupon code ID.
* @returns {Object} - The upserted discount object.
* @throws {Error} - If an error occurs.
*/
async upsertDiscount(data) {
const { entity_type, entity_id, coupon_code_id } = data;
if (!entity_type || !entity_id || !coupon_code_id) {
throw new Error('Discount upsert requires entity_type, entity_id, and coupon_code_id');
}
const where = { entity_type, entity_id, coupon_code_id };
const existing = await this.DB.discountsToEntities.query().findOne(where);
if (existing) {
await this.DB.discountsToEntities.query().findById(existing.id).patch(data);
return await this.DB.discountsToEntities.query().findById(existing.id);
}
return await this.DB.discountsToEntities.query().insert(data);
}
/**
* Get discounts for a transaction or transaction_item.
* @param {Number} transactionId - The transaction ID.
* @param {Number} couponCodeId - The coupon code ID.
* @returns {Object} - The discounts object.
* @throws {Error} - If an error occurs.
*/
async getDiscount({entityType, entityId, couponCodeId = null}) {
const query = this.DB.discountsToEntities.query().where({
entity_type: entityType,
entity_id: entityId,
});
if (couponCodeId) {
query.andWhere('coupon_code_id', couponCodeId);
}
return await query.first();
}
/**
* Get discounts for a transaction item.
* @param {Number} transactionItemId - The transaction item ID.
* @returns {Object} - The discounts object.
* @throws {Error} - If an error occurs.
*/
async getDiscountsForEntities(entityType, entityIds, couponCodeId = null) {
const query = this.DB.discountsToEntities.query()
.where('entity_type', entityType);
if (Array.isArray(entityIds)) {
query.whereIn('entity_id', entityIds);
}
else {
query.where('entity_id', entityIds);
}
if (couponCodeId) {
query.andWhere('coupon_code_id', couponCodeId);
}
return await query;
}
/**
* Get all payment types.
* @returns {Array.<PaymentTypeEntity>} - An array of payment type entities.
* @throws {Error} - If an error occurs.
*/
async getPaymentTypes() {
const paymentTypes = await this.DB.paymentTypes.query();
const entities = [];
for (const paymentType of paymentTypes) {
const entity = new PaymentTypeEntity(paymentType);
entities.push(entity);
}
return entities;
}
/**
* Get all transaction types.
* @returns {Array.<TransactionTypeEntity>} - An array of transaction type entities.
* @throws {Error} - If an error occurs.
*/
async getTransactionTypes() {
const transactionTypes = await this.DB.transactionTypes.query();
const entities = [];
for (const transactionType of transactionTypes) {
const entity = new TransactionTypeEntity(transactionType);
entities.push(entity);
}
return entities;
}
/**
* Get all transaction categories.
* @returns {Array.<TransactionCategoryEntity>} - An array of transaction category entities.
* @throws {Error} - If an error occurs.
*/
async getTransactionCategories() {
const transactionCategories = await this.DB.transactionCategories.query();
const entities = [];
for (const transactionCategory of transactionCategories) {
const entity = new TransactionCategoryEntity(transactionCategory);
entities.push(entity);
}
return entities;
}
#getTransactionDataQuery({ transactionId, orderId, cartId }) {
let whereClause = '';
if (transactionId) {
whereClause = `o.id IN (
SELECT t.order_id FROM transactions t WHERE t.id = ${Number(transactionId)}
)`;
}
else if (orderId) {
whereClause = `o.id = ${Number(orderId)}`;
}
else if (cartId) {
whereClause = `o.cart_id = ${Number(cartId)}`;
}
else {
throw new Error('Must provide one of: transactionId, orderId, or cartId');
}
return `
WITH transaction_data AS (
SELECT
t.id,
o.id AS td_order_id,
c.id AS cart_id,
t.amount,
t.order_id,
t.created_at,
tc.label AS category,
pt.type AS payment_type
FROM transactions t
LEFT JOIN orders o ON o.id = t.order_id
LEFT JOIN transaction_categories tc ON tc.id = t.transaction_category_id
LEFT JOIN payment_types pt ON pt.id = t.payment_type_id
LEFT JOIN carts c ON c.id = o.cart_id
LEFT JOIN transaction_items ti ON ti.transaction_id = t.id
)
SELECT json_build_object(
'date', o.created_at,
'cart', json_build_object(
'id', c.id,
'user_id', c.user_id,
'subtotal', c.subtotal,
'total', c.total,
'status', c.status,
'created_at', c.created_at,
'updated_at', c.updated_at,
'cart_items', COALESCE((
SELECT json_agg(json_build_object(
'id', ci.id,
'cart_id', ci.cart_id,
'entity_type', ci.entity_type,
'entity_id', ci.entity_id,
'created_at', ci.created_at,
'updated_at', ci.updated_at,
'price', ci.price
))
FROM cart_items ci
WHERE ci.cart_id = c.id
), '[]')
),
'order', json_build_object(
'id', o.id,
'cart_id', o.cart_id,
'total_amount', o.total_amount,
'created_at', o.created_at,
'updated_at', o.updated_at,
'order_items', (
SELECT json_agg(json_build_object(
'id', oi.id,
'order_id', oi.order_id,
'entity_type', oi.entity_type,
'entity_id', oi.entity_id,
'cart_item_id', oi.cart_item_id,
'created_at', oi.created_at,
'updated_at', oi.updated_at,
'amount', oi.amount,
'entity_name', (
CASE
WHEN oi.entity_type = 'illustration' THEN (
SELECT illustrations.name
FROM illustrations
WHERE illustrations.id = oi.entity_id
)
WHEN oi.entity_type = 'icon' THEN (
SELECT icons.name
FROM icons
WHERE icons.id = oi.entity_id
)
ELSE NULL
END
)
))
FROM order_items oi
WHERE oi.order_id = o.id
)
),
'transactions', (
SELECT json_agg(json_build_object(
'id', t.id,
'amount', t.amount,
'payment_type_id', t.payment_type_id,
'transaction_category_id', t.transaction_category_id,
'created_at', t.created_at,
'updated_at', t.updated_at,
'transaction_items', (
SELECT json_agg(json_build_object(
'id', ti.id,
'transaction_id', ti.transaction_id,
'transaction_type_id', ti.transaction_type_id,
'payment_type_id', ti.payment_type_id,
'order_item_id', ti.order_item_id,
'account_id', ti.account_id,
'amount', coalesce(ti.amount, 0),
'commission_amount', coalesce(ti.commission_amount, 0),
'memo', ti.memo,
'created_at', ti.created_at,
'updated_at', ti.updated_at
))
FROM transaction_items ti
WHERE ti.transaction_id = t.id
)
))
FROM transactions t
WHERE t.order_id = o.id
)
)
FROM orders o
JOIN carts c ON c.id = o.cart_id
WHERE ${whereClause}
ORDER BY o.created_at DESC;
`;
}
/**
* Get transaction data by transaction ID.
* @param {Number} transactionId - The transaction ID.
* @returns {Object} - The transaction data.
* @throws {Error} - If an error occurs.
*/
getTransactionDataByTransactionId(transactionId) {
const query = this.#getTransactionDataQuery({ transactionId });
return this.DB.knex.raw(query);
}
/**
* Get transaction data by order ID.
* @param {Number} orderId - The order ID.
* @returns {Object} - The transaction data.
* @throws {Error} - If an error occurs.
*/
getTransactionDataByOrderId(orderId) {
const query = this.#getTransactionDataQuery({ orderId });
return this.DB.knex.raw(query);
}
/**
* Get transaction data by cart ID.
* @param {Number} cartId - The cart ID.
* @returns {Object} - The transaction data.
* @throws {Error} - If an error occurs.
*/
getTransactionDataByCartId(cartId) {
const query = this.#getTransactionDataQuery({ cartId });
return this.DB.knex.raw(query);
}
}
module.exports = TransactionRepository;
Source