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
JavaScript
"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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