Реалізація програми лояльності в мобільному додатку
Програма лояльності в мобільному додатку — це не просто «копіюйте бали». За простим інтерфейсом скривається нетривіальна серверна логіка: трансакційність нарахування, запобігання накруткам, expiry балів і синхронізація стану між сесіями. Недооцінювати серверну частину тут не можна.
Модель даних та трансакційність
Баланс балів не можна зберігати як одне число в полі user.points. Потрібен журнал транзакцій:
loyalty_transactions:
id UUID PK
user_id UUID FK
type ENUM('earn', 'redeem', 'expire', 'refund', 'bonus')
amount INTEGER -- позитивне для earn, негативне для redeem
reference_id UUID -- purchase_id, action_id
reference_type VARCHAR
expires_at TIMESTAMP -- для earn-транзакцій
created_at TIMESTAMP
-- Баланс = сума amount по незаистєким транзакціям
-- SELECT COALESCE(SUM(amount), 0) FROM loyalty_transactions
-- WHERE user_id = ? AND (expires_at IS NULL OR expires_at > NOW())
Це дозволяє: відкотити нарахування при повернення покупки, реалізувати expiry з точністю до транзакції, аудировати будь-які зміни баланса.
Усі операції зміни баланса — через транзакцію БД з рівнем ізоляції SERIALIZABLE для лічильників, або через оптимістичну блокування. Без цього при паралельних запитах (кілька вкладок, повторний тап) баланс може піти в мінус.
Механіки нарахування
Базові варіанти: за покупку (N балів за рубль), за дію (реєстрація, відзив, запрошення друга), бонусні періоди (x2 на вихідних). Кожен тип — окрема rule в таблиці loyalty_rules з умовами й коефіцієнтами. Це дозволяє змінювати механіки без деплоя.
Запобігання накруткам: обмеження нарахувань за одну дію в одиницю часу (rate limiting за user_id + action_type), верифікація дій (наприклад, відзив засчитується тільки після модерації), ліміт на реферальні нарахування.
Клієнтська реалізація
На мобільному клієнті — три ключових екрани: баланс з історією транзакцій, каталог вознаграджень, екран списання. Баланс синхронізується при кожному відкритті додатку й через WebSocket/SSE при активних операціях.
// Swift — підписка на оновлення баланса через WebSocket
class LoyaltyViewModel: ObservableObject {
@Published var balance: Int = 0
@Published var transactions: [LoyaltyTransaction] = []
func subscribeToUpdates() {
webSocketService.subscribe(channel: "loyalty.\(userID)") { [weak self] event in
DispatchQueue.main.async {
self?.balance = event.newBalance
self?.transactions.insert(event.transaction, at: 0)
}
}
}
}
Рівні програми лояльності
Тиерна система (Bronze → Silver → Gold) вимагає перерахування рівня при кожній зміні баланса. Краще зберігати total_earned (накопичене за період без урахування списань) окремо від поточного баланса — саме за цим значенням визначається tier.
Дати скидання tier (звичайно щорічно) — окрема фонова задача або cron. Потрібно повідомляти користувачів за 30 днів до даунгрейду tier.
Інтеграція з IAP
При покупці через In-App Purchase: нарахування балів відбувається на сервері після верифікації транзакції, до finishTransaction на клієнті чекати не потрібно — балі можна нараховувати асинхронно. При refund через Apple/Google — обробляємо webhook й створюємо refund-транзакцію в журналі.
Терміни реалізації — приблизно 5 днів: проектування схеми, серверна логіка нарахування й списання, клієнтські екрани, сповіщення.







