UNPKG

medusa-bonuses-plugin

Version:

medusa-bonuses-plugin is a Medusa plugin that adds bonus program to Medusa ecommerce stores.

1,016 lines 76.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); // src/services/bonus.ts const medusa_1 = require("@medusajs/medusa"); const bonus_account_1 = require("../models/bonus-account"); const bonus_settings_1 = require("../models/bonus-settings"); const bonus_transaction_1 = require("../models/bonus-transaction"); const medusa_core_utils_1 = require("medusa-core-utils"); class BonusService extends medusa_1.TransactionBaseService { constructor(container) { super(container); const manager = container.manager; this.bonusAccountRepository_ = manager.getRepository(bonus_account_1.BonusAccount); this.bonusTransactionRepository_ = manager.getRepository(bonus_transaction_1.BonusTransaction); this.bonusSettingsRepository_ = manager.getRepository(bonus_settings_1.BonusSettings); this.userService_ = container.userService; this.cartService_ = container.cartService; this.discountService_ = container.discountService; this.customerService_ = container.customerService; } // Settings Management async getSettings() { const settings = await this.bonusSettingsRepository_.find({})[0]; if (!settings) { return this.bonusSettingsRepository_.save(this.bonusSettingsRepository_.create({ default_percentage: 3 })); } return settings; } async updateSettings(data) { // Валидация процента if (data.default_percentage !== undefined) { if (data.default_percentage < 0 || data.default_percentage > 100) { throw new medusa_core_utils_1.MedusaError(medusa_core_utils_1.MedusaError.Types.INVALID_DATA, "Default percentage must be between 0 and 100"); } } return await this.atomicPhase_(async (manager) => { const settings = await this.getSettings(); for (const [key, value] of Object.entries(data)) { if (value !== undefined) { settings[key] = value; } } return await this.bonusSettingsRepository_.save(settings); }); } // Account Management async getBonusAccount(customerId, config = {}) { const accountRepo = this.manager_.getRepository(bonus_account_1.BonusAccount); const query = (0, medusa_1.buildQuery)({ customer_id: customerId }, config); const account = await accountRepo.findOne(query); if (!account) { throw new medusa_core_utils_1.MedusaError(medusa_core_utils_1.MedusaError.Types.NOT_FOUND, `Bonus account for customer ${customerId} was not found`); } return account; } async createBonusAccount(customerId, data) { return await this.atomicPhase_(async (manager) => { const existing = await this.bonusAccountRepository_.findOne({ where: { customer_id: customerId } }); if (existing) { return existing; } const created = this.bonusAccountRepository_.create({ customer_id: customerId, balance: 0, metadata: data === null || data === void 0 ? void 0 : data.metadata }); return await this.bonusAccountRepository_.save(created); }); } async updateBonusAccount(customerId, update) { return await this.atomicPhase_(async (manager) => { const accountRepo = manager.getRepository(bonus_account_1.BonusAccount); const bonusAccount = await this.getBonusAccount(customerId); for (const [key, value] of Object.entries(update)) { if (value !== undefined) { if (key === 'metadata' && typeof value === 'object' && value !== null) { bonusAccount.metadata = { ...(bonusAccount.metadata || {}), ...value }; } else { bonusAccount[key] = value; } } } const updated = await accountRepo.save(bonusAccount); if (update.balance !== undefined) { const balanceChange = update.balance - (bonusAccount.balance || 0); if (balanceChange !== 0) { await this.createTransaction({ customer_id: customerId, amount: balanceChange, type: bonus_transaction_1.BonusTransactionType.ADJUSTMENT, note: `Manual balance adjustment: ${balanceChange}`, metadata: { previous_balance: bonusAccount.balance, new_balance: update.balance, adjusted_by: "system", adjustment_reason: "account_update" } }); } } return updated; }); } async adjustBonusBalance(customerId, balance, metadata) { // Валидация баланса if (balance < 0) { throw new medusa_core_utils_1.MedusaError(medusa_core_utils_1.MedusaError.Types.INVALID_DATA, "Balance cannot be negative"); } return await this.atomicPhase_(async (manager) => { const user = await this.userService_.retrieve(metadata.adjusted_by); const bonusAccount = await this.getBonusAccount(customerId); if (user && bonusAccount) { const balanceChange = balance - (bonusAccount.balance || 0); if (balanceChange !== 0) { await this.createTransaction({ customer_id: customerId, amount: balanceChange, type: bonus_transaction_1.BonusTransactionType.ADJUSTMENT, note: `Manual balance adjustment: ${balanceChange}`, metadata: { previous_balance: bonusAccount.balance, new_balance: balance, adjusted_by: `${user.last_name} ${user.first_name} - ${user.email}`, adjustment_reason: metadata.reason || "manual_adjustment" } }); } // Обновляем баланс bonusAccount.balance = balance; await this.bonusAccountRepository_.save(bonusAccount); } return await this.getBonusAccount(customerId); }); } // Customer Bonus Percentage Management async setCustomerBonusPercentage(customerId, percentage, options) { // Валидация процента if (percentage < 0 || percentage > 100) { throw new medusa_core_utils_1.MedusaError(medusa_core_utils_1.MedusaError.Types.INVALID_DATA, "Percentage must be between 0 and 100"); } return await this.atomicPhase_(async (manager) => { const account = await this.getBonusAccount(customerId); account.custom_percentage = percentage; account.custom_percentage_ends_at = (options === null || options === void 0 ? void 0 : options.ends_at) || null; if (options === null || options === void 0 ? void 0 : options.metadata) { account.metadata = { ...account.metadata, ...options.metadata, custom_percentage_set_at: new Date() }; } return await this.bonusAccountRepository_.save(account); }); } async removeCustomerBonusPercentage(customerId) { return await this.atomicPhase_(async (manager) => { const account = await this.getBonusAccount(customerId); account.custom_percentage = null; account.custom_percentage_ends_at = null; return await this.bonusAccountRepository_.save(account); }); } async getCurrentBonusPercentage(customerId) { const [account, settings] = await Promise.all([ this.getBonusAccount(customerId), this.getSettings() ]); if (account.custom_percentage !== null && (!account.custom_percentage_ends_at || account.custom_percentage_ends_at > new Date())) { return account.custom_percentage; } return settings.default_percentage; } // Transaction Management async createTransaction(data) { return await this.atomicPhase_(async (manager) => { const bonusAccount = await this.getBonusAccount(data.customer_id); const transaction = this.bonusTransactionRepository_.create({ bonus_account_id: bonusAccount.customer_id, customer_id: data.customer_id, amount: data.amount, type: data.type, order_id: data.order_id, order_relation_type: data.order_relation_type, order_amount: data.order_amount, earning_percentage: data.earning_percentage, created_by: data.created_by, note: data.note, metadata: data.metadata }); return await this.bonusTransactionRepository_.save(transaction); }); } // Order Bonus Operations async addOrderEarning(orderId, customerId, amount) { return await this.atomicPhase_(async (manager) => { const bonusAccount = await this.getBonusAccount(customerId); const percentage = await this.getCurrentBonusPercentage(customerId); // Создаем транзакцию начисления await this.createTransaction({ customer_id: customerId, amount, type: bonus_transaction_1.BonusTransactionType.ORDER_EARNING, order_id: orderId, order_relation_type: bonus_transaction_1.OrderRelationType.EARNED_FROM, earning_percentage: percentage, note: `Earned ${amount} bonus points from order ${orderId}` }); // Обновляем баланс bonusAccount.balance += amount; await this.bonusAccountRepository_.save(bonusAccount); }); } async spendOnOrder(orderId, customerId, amount) { return await this.atomicPhase_(async (manager) => { const bonusAccount = await this.getBonusAccount(customerId); if (bonusAccount.balance < amount) { throw new medusa_core_utils_1.MedusaError(medusa_core_utils_1.MedusaError.Types.NOT_ALLOWED, `Insufficient bonus balance. Required: ${amount}, Available: ${bonusAccount.balance}`); } // Создаем транзакцию списания await this.createTransaction({ customer_id: customerId, amount: -amount, type: bonus_transaction_1.BonusTransactionType.ORDER_SPENDING, order_id: orderId, order_relation_type: bonus_transaction_1.OrderRelationType.SPENT_ON, note: `Spent ${amount} bonus points on order ${orderId}` }); // Обновляем баланс bonusAccount.balance -= amount; await this.bonusAccountRepository_.save(bonusAccount); }); } // Order Validation and Calculations async isEligibleForBonus(order) { var _a, _b; // Проверяем условия начисления бонусов if (!order.customer_id) { return false; } // Проверяем что пользователь подтвержден const customer = await this.customerService_.retrieve(order.customer_id); // if (!customer.has_account || !customer.metadata?.email_verified) { if (!customer.has_account) { return false; } // Проверяем что нет товаров со скидкой (акционных) if ((_a = order.items) === null || _a === void 0 ? void 0 : _a.some(item => { var _a; const product = (_a = item.variant) === null || _a === void 0 ? void 0 : _a.product; return (product === null || product === void 0 ? void 0 : product.discountable) || item.discount_total > 0; })) { return false; } // Проверяем что нет примененных промокодов (кроме бонусных) if ((_b = order.discounts) === null || _b === void 0 ? void 0 : _b.some(d => { var _a; return ((_a = d.metadata) === null || _a === void 0 ? void 0 : _a.type) !== "bonus_discount"; })) { return false; } return true; } async isEligibleForBonusPayment(cart) { var _a, _b; if (!cart.customer_id) { return false; } // Проверяем что пользователь подтвержден const customer = await this.customerService_.retrieve(cart.customer_id); if (!customer.has_account) { //|| !customer.metadata?.email_verified) { return false; } // Проверяем что нет товаров со скидкой if ((_a = cart.items) === null || _a === void 0 ? void 0 : _a.some(item => { var _a; const product = (_a = item.variant) === null || _a === void 0 ? void 0 : _a.product; return (product === null || product === void 0 ? void 0 : product.discountable) || item.discount_total > 0; })) { return false; } // Проверяем что нет примененных промокодов if (((_b = cart.discounts) === null || _b === void 0 ? void 0 : _b.length) > 0) { return false; } return true; } async calculateOrderBonus(order) { if (!await this.isEligibleForBonus(order)) { return 0; } const percentage = await this.getCurrentBonusPercentage(order.customer_id); return Math.floor(order.total * (percentage / 100)); } // Transaction History async getTransactionHistory(customerId, options = {}) { var _a; const query = this.bonusTransactionRepository_ .createQueryBuilder("transaction") .leftJoinAndSelect("transaction.order", "order") .where("transaction.customer_id = :customerId", { customerId }); if ((_a = options.type) === null || _a === void 0 ? void 0 : _a.length) { query.andWhere("transaction.type IN (:...types)", { types: options.type }); } if (options.fromDate) { query.andWhere("transaction.created_at >= :fromDate", { fromDate: options.fromDate }); } if (options.toDate) { query.andWhere("transaction.created_at <= :toDate", { toDate: options.toDate }); } query .orderBy("transaction.created_at", options.order || "DESC") .skip(options.offset || 0) .take(options.limit || 20); const [transactions, count] = await query.getManyAndCount(); return { transactions, count }; } async getOrderRelatedTransactions(orderId) { const transactions = await this.bonusTransactionRepository_.find({ where: { order_id: orderId }, relations: ["customer", "bonus_account"] }); return { earnings: transactions.find(t => t.order_relation_type === bonus_transaction_1.OrderRelationType.EARNED_FROM), spendings: transactions.find(t => t.order_relation_type === bonus_transaction_1.OrderRelationType.SPENT_ON) }; } // Резервирование бонусов async reserveBonus(customerId, amount, referenceId) { return await this.atomicPhase_(async (manager) => { const bonusAccount = await this.getBonusAccount(customerId); if (bonusAccount.balance < amount) { throw new medusa_core_utils_1.MedusaError(medusa_core_utils_1.MedusaError.Types.NOT_ALLOWED, `Insufficient bonus balance for reservation`); } // Создаем транзакцию резервирования const transaction = await this.createTransaction({ customer_id: customerId, amount: -amount, type: bonus_transaction_1.BonusTransactionType.RESERVATION, note: `Reserved ${amount} bonus points`, metadata: { reference_id: referenceId, reserved_at: new Date() } }); // Обновляем баланс bonusAccount.balance -= amount; await this.bonusAccountRepository_.save(bonusAccount); return transaction; }); } // Отмена резервирования async cancelReservation(customerId, referenceId) { return await this.atomicPhase_(async (manager) => { // Находим транзакцию резервирования const reservation = await this.bonusTransactionRepository_.findOne({ where: { customer_id: customerId, type: bonus_transaction_1.BonusTransactionType.RESERVATION, metadata: { reference_id: referenceId } } }); if (!reservation) { return; } // Возвращаем бонусы await this.createTransaction({ customer_id: customerId, amount: Math.abs(reservation.amount), type: bonus_transaction_1.BonusTransactionType.ADJUSTMENT, note: `Canceled reservation`, metadata: { original_reservation_id: reservation.id, reference_id: referenceId } }); // Обновляем баланс const bonusAccount = await this.getBonusAccount(customerId); bonusAccount.balance += Math.abs(reservation.amount); await this.bonusAccountRepository_.save(bonusAccount); }); } async applyBonusToCart(customerId, cartId, requestedAmount) { return await this.atomicPhase_(async (manager) => { const bonusAccount = await this.getBonusAccount(customerId); const cart = await this.cartService_.retrieve(cartId, { relations: ["customer", "items", "items.variant", "items.variant.product", "discounts"] }); // Проверяем возможность применения бонусов if (!await this.isEligibleForBonusPayment(cart)) { throw new medusa_core_utils_1.MedusaError(medusa_core_utils_1.MedusaError.Types.NOT_ALLOWED, "Cart is not eligible for bonus payment"); } // Если сумма не указана - используем максимально возможную const amountToApply = requestedAmount !== null && requestedAmount !== void 0 ? requestedAmount : Math.min(bonusAccount.balance, cart.total); if (amountToApply <= 0 || amountToApply > bonusAccount.balance) { throw new medusa_core_utils_1.MedusaError(medusa_core_utils_1.MedusaError.Types.INVALID_DATA, "Invalid bonus amount"); } try { const discountCode = await this.createBonusDiscount(cartId, amountToApply, customerId); return { applied: true, appliedAmount: amountToApply, discountCode }; } catch (error) { throw new medusa_core_utils_1.MedusaError(medusa_core_utils_1.MedusaError.Types.UNEXPECTED_STATE, `Failed to apply bonus: ${error.message}`); } }); } async createBonusDiscount(cartId, bonusAmount, customerId) { return await this.atomicPhase_(async (manager) => { // Создаём временную скидку для использования бонусов const discount = await this.discountService_ .withTransaction(manager) .create({ code: `BONUS_${customerId}_${Date.now()}`, rule: { type: medusa_1.DiscountRuleType.FIXED, value: bonusAmount, allocation: medusa_1.AllocationType.TOTAL }, is_disabled: false, is_dynamic: true, valid_duration: "P1D", // 1 день usage_limit: 1, metadata: { type: "bonus_discount", customer_id: customerId, bonus_amount: bonusAmount, cart_id: cartId } }); // Резервируем бонусы await this.reserveBonus(customerId, bonusAmount, discount.code); return discount.code; }); } // Активация бонусов через промокод async activateBonusTransfer(customerId, transferCode, transferData) { return await this.atomicPhase_(async (manager) => { // Проверяем, не был ли этот код уже использован const existingTransfer = await this.bonusTransactionRepository_.findOne({ where: { customer_id: customerId, metadata: { transfer_code: transferCode } } }); if (existingTransfer) { throw new medusa_core_utils_1.MedusaError(medusa_core_utils_1.MedusaError.Types.DUPLICATE_ERROR, "This transfer code has already been used"); } // Валидация суммы if (transferData.amount <= 0) { throw new medusa_core_utils_1.MedusaError(medusa_core_utils_1.MedusaError.Types.INVALID_DATA, "Transfer amount must be positive"); } // Создаем транзакцию перевода await this.createTransaction({ customer_id: customerId, amount: transferData.amount, type: bonus_transaction_1.BonusTransactionType.ADJUSTMENT, note: `Bonus transfer from ${transferData.source || 'external source'}`, metadata: { transfer_code: transferCode, transfer_source: transferData.source, transfer_date: new Date() } }); // Обновляем баланс const bonusAccount = await this.getBonusAccount(customerId); bonusAccount.balance += transferData.amount; await this.bonusAccountRepository_.save(bonusAccount); return bonusAccount; }); } } exports.default = BonusService; // src/services/bonus.ts // import { // Customer, // Order, // TransactionBaseService, // FindConfig, // Selector, // buildQuery, // CartService, // DiscountRuleType, // DiscountService, // Cart, // AllocationType, // CustomerService, // UserService // } from "@medusajs/medusa" // import { EntityManager, Repository } from "typeorm" // import { BonusAccount } from "../models/bonus-account" // import { BonusSettings } from "../models/bonus-settings" // import { // BonusTransaction, // BonusTransactionType, // OrderRelationType // } from "../models/bonus-transaction" // import { MedusaError } from "medusa-core-utils" // type InjectedDependencies = { // manager: EntityManager // bonusAccountRepository: Repository<BonusAccount> // bonusTransactionRepository: Repository<BonusTransaction> // bonusSettingsRepository: Repository<BonusSettings> // userService: UserService // cartService: CartService // discountService: DiscountService // } // class BonusService extends TransactionBaseService { // protected readonly bonusAccountRepository_: Repository<BonusAccount> // protected readonly bonusTransactionRepository_: Repository<BonusTransaction> // protected readonly bonusSettingsRepository_: Repository<BonusSettings> // protected readonly userService_: UserService; // protected readonly cartService_: CartService; // protected readonly discountService_: DiscountService; // constructor(container: InjectedDependencies) { // super(container) // const manager = container.manager // this.bonusAccountRepository_ = manager.getRepository(BonusAccount) // this.bonusTransactionRepository_ = manager.getRepository(BonusTransaction) // this.bonusSettingsRepository_ = manager.getRepository(BonusSettings) // this.userService_ = container.userService // this.cartService_ = container.cartService; // this.discountService_ = container.discountService; // } // // Settings Management // async getSettings(): Promise<BonusSettings> { // const settings = await this.bonusSettingsRepository_.findOne({}) // if (!settings) { // return this.bonusSettingsRepository_.save( // this.bonusSettingsRepository_.create({ // default_percentage: 3 // }) // ) // } // return settings // } // async updateSettings(data: { // default_percentage?: number // metadata?: Record<string, unknown> // }): Promise<BonusSettings> { // return await this.atomicPhase_(async (manager) => { // const settings = await this.getSettings() // for (const [key, value] of Object.entries(data)) { // if (value !== undefined) { // settings[key] = value // } // } // return await this.bonusSettingsRepository_.save(settings) // }) // } // // Account Management // async getBonusAccount( // customerId: string, // config: FindConfig<BonusAccount> = {} // ): Promise<BonusAccount> { // const accountRepo = this.manager_.getRepository(BonusAccount) // const query = buildQuery( // { customer_id: customerId }, // config // ) // const account = await accountRepo.findOne(query) // if (!account) { // throw new MedusaError( // MedusaError.Types.NOT_FOUND, // `Bonus account for customer ${customerId} was not found` // ) // } // return account // } // async createBonusAccount( // customerId: string, // data?: { // metadata?: Record<string, unknown> // } // ): Promise<BonusAccount> { // return await this.atomicPhase_(async (manager) => { // const existing = await this.bonusAccountRepository_.findOne({ // where: { customer_id: customerId } // }) // if (existing) { // return existing // } // const created = this.bonusAccountRepository_.create({ // customer_id: customerId, // balance: 0, // metadata: data?.metadata // }) // return await this.bonusAccountRepository_.save(created) // }) // } // async updateBonusAccount( // customerId: string, // update: { // balance?: number // custom_percentage?: number | null // custom_percentage_ends_at?: Date | null // metadata?: Record<string, unknown> // } // ): Promise<BonusAccount> { // return await this.atomicPhase_(async (manager) => { // const accountRepo = manager.getRepository(BonusAccount) // const bonusAccount = await this.getBonusAccount(customerId) // for (const [key, value] of Object.entries(update)) { // if (value !== undefined) { // if (key === 'metadata' && typeof value === 'object' && value !== null) { // bonusAccount.metadata = { // ...(bonusAccount.metadata || {}), // ...(value as Record<string, unknown>) // } // } else { // (bonusAccount as any)[key] = value // } // } // } // const updated = await accountRepo.save(bonusAccount) // if (update.balance !== undefined) { // const balanceChange = update.balance - (bonusAccount.balance || 0) // if (balanceChange !== 0) { // await this.createTransaction({ // customer_id: customerId, // amount: balanceChange, // type: BonusTransactionType.ADJUSTMENT, // note: `Manual balance adjustment: ${balanceChange}`, // metadata: { // previous_balance: bonusAccount.balance, // new_balance: update.balance, // adjusted_by: "system", // adjustment_reason: "account_update" // } // }) // } // } // return updated // }) // } // async adjustBonusBalance( // customerId: string, // balance: number, // metadata: { // reason?: string, // adjusted_by: string // } // ): Promise<BonusAccount> { // return await this.atomicPhase_(async (manager) => { // const user = await this.userService_.retrieve(metadata.adjusted_by) // const bonusAccount = await this.getBonusAccount(customerId) // if(user && bonusAccount) { // if (balance !== undefined && balance > 0) { // const balanceChange = balance - (bonusAccount.balance || 0) // if (balanceChange !== 0) { // await this.createTransaction({ // customer_id: customerId, // amount: balanceChange, // type: BonusTransactionType.ADJUSTMENT, // note: `Manual balance adjustment: ${balanceChange}`, // metadata: { // previous_balance: bonusAccount.balance, // new_balance: balance, // adjusted_by: metadata.adjusted_by ? `${user.last_name} ${user.first_name} - ${user.email}` : "system", // adjustment_reason: metadata.reason ? metadata.reason : "account_update" // } // }) // } // } // } // return await this.getBonusAccount(customerId) // }) // } // // Customer Bonus Percentage Management // async setCustomerBonusPercentage( // customerId: string, // percentage: number, // options?: { // ends_at?: Date // metadata?: Record<string, unknown> // } // ): Promise<BonusAccount> { // return await this.atomicPhase_(async (manager) => { // const account = await this.getBonusAccount(customerId) // account.custom_percentage = percentage // account.custom_percentage_ends_at = options?.ends_at || null // if (options?.metadata) { // account.metadata = { // ...account.metadata, // ...options.metadata // } // } // return await this.bonusAccountRepository_.save(account) // }) // } // async removeCustomerBonusPercentage(customerId: string): Promise<BonusAccount> { // return await this.atomicPhase_(async (manager) => { // const account = await this.getBonusAccount(customerId) // account.custom_percentage = null // account.custom_percentage_ends_at = null // return await this.bonusAccountRepository_.save(account) // }) // } // async getCurrentBonusPercentage(customerId: string): Promise<number> { // const [account, settings] = await Promise.all([ // this.getBonusAccount(customerId), // this.getSettings() // ]) // if ( // account.custom_percentage !== null && // (!account.custom_percentage_ends_at || // account.custom_percentage_ends_at > new Date()) // ) { // return account.custom_percentage // } // return settings.default_percentage // } // // Transaction Management // async createTransaction(data: { // customer_id: string // amount: number // type: BonusTransactionType // order_id?: string // order_relation_type?: OrderRelationType // order_amount?: number // earning_percentage?: number // created_by?: string // note?: string // metadata?: Record<string, unknown> // }): Promise<BonusTransaction> { // return await this.atomicPhase_(async (manager) => { // const bonusAccount = await this.getBonusAccount(data.customer_id) // const transaction = this.bonusTransactionRepository_.create({ // bonus_account_id: bonusAccount.customer_id, // customer_id: data.customer_id, // amount: data.amount, // type: data.type, // order_id: data.order_id, // order_relation_type: data.order_relation_type, // order_amount: data.order_amount, // earning_percentage: data.earning_percentage, // created_by: data.created_by, // note: data.note, // metadata: data.metadata // }) // return await this.bonusTransactionRepository_.save(transaction) // }) // } // // Order Bonus Operations // async addOrderEarning( // orderId: string, // customerId: string, // amount: number // ): Promise<void> { // return await this.atomicPhase_(async (manager) => { // const bonusAccount = await this.getBonusAccount(customerId) // const percentage = await this.getCurrentBonusPercentage(customerId) // // Создаем транзакцию начисления // await this.createTransaction({ // customer_id: customerId, // amount, // type: BonusTransactionType.ORDER_EARNING, // order_id: orderId, // order_relation_type: OrderRelationType.EARNED_FROM, // earning_percentage: percentage, // note: `Earned ${amount} bonus points from order ${orderId}` // }) // // Обновляем баланс // bonusAccount.balance += amount // await this.bonusAccountRepository_.save(bonusAccount) // }) // } // async spendOnOrder( // orderId: string, // customerId: string, // amount: number // ): Promise<void> { // return await this.atomicPhase_(async (manager) => { // const bonusAccount = await this.getBonusAccount(customerId) // if (bonusAccount.balance < amount) { // throw new MedusaError( // MedusaError.Types.NOT_ALLOWED, // `Insufficient bonus balance. Required: ${amount}, Available: ${bonusAccount.balance}` // ) // } // // Создаем транзакцию списания // await this.createTransaction({ // customer_id: customerId, // amount: -amount, // type: BonusTransactionType.ORDER_SPENDING, // order_id: orderId, // order_relation_type: OrderRelationType.SPENT_ON, // note: `Spent ${amount} bonus points on order ${orderId}` // }) // // Обновляем баланс // bonusAccount.balance -= amount // await this.bonusAccountRepository_.save(bonusAccount) // }) // } // // Резервирование бонусов (недостающий метод) // async reserveBonus( // customerId: string, // amount: number, // discountCode: string // ): Promise<void> { // return await this.atomicPhase_(async (manager) => { // const bonusAccount = await this.getBonusAccount(customerId) // if (bonusAccount.balance < amount) { // throw new MedusaError( // MedusaError.Types.NOT_ALLOWED, // `Insufficient bonus balance for reservation. Required: ${amount}, Available: ${bonusAccount.balance}` // ) // } // // Создаем транзакцию резервирования // await this.createTransaction({ // customer_id: customerId, // amount: -amount, // type: BonusTransactionType.RESERVATION, // note: `Reserved ${amount} bonus points for discount ${discountCode}`, // metadata: { // discount_code: discountCode, // reserved_at: new Date() // } // }) // // Обновляем баланс (временно вычитаем зарезервированные бонусы) // bonusAccount.balance -= amount // await this.bonusAccountRepository_.save(bonusAccount) // }) // } // // Order Validation and Calculations // async isEligibleForBonus(order: Order): Promise<boolean> { // // Проверяем условия начисления бонусов // if (!order.customer_id) { // return false // } // // Проверяем что пользователь подтвержден // if (!order.customer?.has_account) { // return false // } // // Проверяем что нет товаров со скидкой // if (order.items?.some(item => item.variant?.product?.discountable)) { // return false // } // // Проверяем что нет примененных промокодов // if (order.discounts?.length > 0) { // return false // } // return true // } // async isEligibleForBonusPayment(cart: Cart): Promise<boolean> { // if(!cart.customer_id) { // return false // } // // Проверяем что пользователь подтвержден // if (!cart.customer?.has_account) { // return false // } // // Проверяем что нет товаров со скидкой // if (cart.items?.some(item => item.variant?.product?.discountable)) { // return false // } // // Проверяем что нет примененных промокодов // if (cart.discounts?.length > 0) { // return false // } // return true // } // async calculateOrderBonus(order: Order): Promise<number> { // if (!await this.isEligibleForBonus(order)) { // return 0 // } // const percentage = await this.getCurrentBonusPercentage(order.customer_id) // return Math.floor(order.total * (percentage / 100)) // } // // Transaction History // async getTransactionHistory( // customerId: string, // options: { // type?: BonusTransactionType[] // limit?: number // offset?: number // order?: "ASC" | "DESC" // fromDate?: Date // toDate?: Date // } = {} // ): Promise<{ // transactions: BonusTransaction[] // count: number // }> { // const query = this.bonusTransactionRepository_ // .createQueryBuilder("transaction") // .leftJoinAndSelect("transaction.order", "order") // .where("transaction.customer_id = :customerId", { customerId }) // if (options.type?.length) { // query.andWhere("transaction.type IN (:...types)", { types: options.type }) // } // if (options.fromDate) { // query.andWhere("transaction.created_at >= :fromDate", { fromDate: options.fromDate }) // } // if (options.toDate) { // query.andWhere("transaction.created_at <= :toDate", { toDate: options.toDate }) // } // query // .orderBy("transaction.created_at", options.order || "DESC") // .skip(options.offset || 0) // .take(options.limit || 20) // const [transactions, count] = await query.getManyAndCount() // return { transactions, count } // } // async getOrderRelatedTransactions( // orderId: string // ): Promise<{ // earnings?: BonusTransaction // spendings?: BonusTransaction // }> { // const transactions = await this.bonusTransactionRepository_.find({ // where: { order_id: orderId }, // relations: ["customer", "bonus_account"] // }) // return { // earnings: transactions.find(t => // t.order_relation_type === OrderRelationType.EARNED_FROM // ), // spendings: transactions.find(t => // t.order_relation_type === OrderRelationType.SPENT_ON // ) // } // } // async applyBonusToCart( // customerId: string, // cartId: string, // requestedAmount?: number // ): Promise<{ // applied: boolean; // appliedAmount: number; // discountCode?: string; // }> { // return await this.atomicPhase_(async (manager) => { // const bonusAccount = await this.getBonusAccount(customerId) // const cart = await this.cartService_.retrieve(cartId) // // Проверяем возможность применения бонусов // if (!this.isEligibleForBonusPayment(cart)) { // throw new MedusaError( // MedusaError.Types.NOT_ALLOWED, // "Cart is not eligible for bonus payment" // ) // } // // Если сумма не указана - используем максимально возможную // const amountToApply = requestedAmount ?? Math.min(bonusAccount.balance, cart.total) // if (amountToApply <= 0 || amountToApply > bonusAccount.balance) { // throw new MedusaError( // MedusaError.Types.INVALID_DATA, // "Invalid bonus amount" // ) // } // try { // const discountCode = await this.createBonusDiscount( // cartId, // amountToApply, // customerId // ) // return { // applied: true, // appliedAmount: amountToApply, // discountCode // } // } catch (error) { // return { // applied: false, // appliedAmount: 0 // } // } // }) // } // async createBonusDiscount( // cartId: string, // bonusAmount: number, // customerId: string // ): Promise<string> { // return await this.atomicPhase_(async (manager) => { // // Создаём временную скидку для использования бонусов // const discount = await this.discountService_ // .withTransaction(manager) // .create({ // code: `BONUS_${customerId}_${Date.now()}`, // rule: { // type: DiscountRuleType.FIXED, // value: bonusAmount, // allocation: AllocationType.TOTAL // }, // is_disabled: false, // is_dynamic: true, // valid_duration: "1 hour", // metadata: { // type: "bonus_discount", // customer_id: customerId, // bonus_amount: bonusAmount, // cart_id: cartId // } // }) // // Резервируем бонусы // await this.reserveBonus( // customerId, // bonusAmount, // discount.code // ) // return discount.code // }) // } // } // export default BonusService //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYm9udXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvc2VydmljZXMvYm9udXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSx3QkFBd0I7QUFDeEIsNkNBY3lCO0FBRXpCLDJEQUFzRDtBQUN0RCw2REFBd0Q7QUFDeEQsbUVBSW9DO0FBQ3BDLHlEQUErQztBQWEvQyxNQUFNLFlBQWEsU0FBUSwrQkFBc0I7SUFTN0MsWUFBWSxTQUErQjtRQUN2QyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUE7UUFDaEIsTUFBTSxPQUFPLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBQTtRQUNqQyxJQUFJLENBQUMsdUJBQXVCLEdBQUcsT0FBTyxDQUFDLGFBQWEsQ0FBQyw0QkFBWSxDQUFDLENBQUE7UUFDbEUsSUFBSSxDQUFDLDJCQUEyQixHQUFHLE9BQU8sQ0FBQyxhQUFhLENBQUMsb0NBQWdCLENBQUMsQ0FBQTtRQUMxRSxJQUFJLENBQUMsd0JBQXdCLEdBQUcsT0FBTyxDQUFDLGFBQWEsQ0FBQyw4QkFBYSxDQUFDLENBQUE7UUFDcEUsSUFBSSxDQUFDLFlBQVksR0FBRyxTQUFTLENBQUMsV0FBVyxDQUFBO1FBQ3pDLElBQUksQ0FBQyxZQUFZLEdBQUcsU0FBUyxDQUFDLFdBQVcsQ0FBQTtRQUN6QyxJQUFJLENBQUMsZ0JBQWdCLEdBQUcsU0FBUyxDQUFDLGVBQWUsQ0FBQTtRQUNqRCxJQUFJLENBQUMsZ0JBQWdCLEdBQUcsU0FBUyxDQUFDLGVBQWUsQ0FBQTtJQUNyRCxDQUFDO0lBRUQsc0JBQXNCO0lBQ3RCLEtBQUssQ0FBQyxXQUFXO1FBQ2IsTUFBTSxRQUFRLEdBQUcsTUFBTSxJQUFJLENBQUMsd0JBQXdCLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFBO1FBQ2hFLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUNaLE9BQU8sSUFBSSxDQUFDLHdCQUF3QixDQUFDLElBQUksQ0FDckMsSUFBSSxD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