mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
201 lines
6.2 KiB
Go
201 lines
6.2 KiB
Go
// internal/services/billing/service.go
|
||
|
||
package billing
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"time"
|
||
|
||
"rmser/internal/domain/account"
|
||
"rmser/internal/domain/billing"
|
||
"rmser/internal/infrastructure/yookassa"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/shopspring/decimal"
|
||
)
|
||
|
||
// Список доступных тарифов
|
||
var AvailableTariffs = []billing.Tariff{
|
||
{ID: "pack_10", Name: "Пакет 10", Type: billing.TariffPack, Price: 500, InvoicesCount: 10, DurationDays: 30},
|
||
{ID: "pack_50", Name: "Пакет 50", Type: billing.TariffPack, Price: 1000, InvoicesCount: 50, DurationDays: 180},
|
||
{ID: "pack_100", Name: "Пакет 100", Type: billing.TariffPack, Price: 1500, InvoicesCount: 100, DurationDays: 365},
|
||
{ID: "sub_7", Name: "Безлимит 7 дней", Type: billing.TariffSubscription, Price: 700, InvoicesCount: 1000, DurationDays: 7},
|
||
{ID: "sub_14", Name: "Безлимит 14 дней", Type: billing.TariffSubscription, Price: 1300, InvoicesCount: 1000, DurationDays: 14},
|
||
{ID: "sub_30", Name: "Безлимит 30 дней", Type: billing.TariffSubscription, Price: 2500, InvoicesCount: 1000, DurationDays: 30},
|
||
}
|
||
|
||
// PaymentNotifier определяет интерфейс для уведомления о успешном платеже
|
||
type PaymentNotifier interface {
|
||
NotifySuccess(userID uuid.UUID, amount float64, newBalance int, serverName string)
|
||
}
|
||
|
||
type Service struct {
|
||
billingRepo billing.Repository
|
||
accountRepo account.Repository
|
||
ykClient *yookassa.Client
|
||
notifier PaymentNotifier
|
||
}
|
||
|
||
func (s *Service) SetNotifier(n PaymentNotifier) {
|
||
s.notifier = n
|
||
}
|
||
|
||
func NewService(bRepo billing.Repository, aRepo account.Repository, yk *yookassa.Client) *Service {
|
||
return &Service{
|
||
billingRepo: bRepo,
|
||
accountRepo: aRepo,
|
||
ykClient: yk,
|
||
}
|
||
}
|
||
|
||
func (s *Service) GetTariffs() []billing.Tariff {
|
||
return AvailableTariffs
|
||
}
|
||
|
||
// CreateOrder теперь возвращает (*billing.Order, string, error)
|
||
func (s *Service) CreateOrder(ctx context.Context, userID uuid.UUID, tariffID string, targetServerURL string, returnURL string) (*billing.Order, string, error) {
|
||
// 1. Ищем тариф
|
||
var selectedTariff *billing.Tariff
|
||
for _, t := range AvailableTariffs {
|
||
if t.ID == tariffID {
|
||
selectedTariff = &t
|
||
break
|
||
}
|
||
}
|
||
if selectedTariff == nil {
|
||
return nil, "", errors.New("тариф не найден")
|
||
}
|
||
|
||
// 2. Определяем целевой сервер
|
||
var targetServerID uuid.UUID
|
||
if targetServerURL != "" {
|
||
srv, err := s.accountRepo.GetServerByURL(targetServerURL)
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("сервер не найден: %w", err)
|
||
}
|
||
targetServerID = srv.ID
|
||
} else {
|
||
srv, err := s.accountRepo.GetActiveServer(userID)
|
||
if err != nil || srv == nil {
|
||
return nil, "", errors.New("активный сервер не выбран")
|
||
}
|
||
targetServerID = srv.ID
|
||
}
|
||
|
||
// 3. Создаем заказ в БД
|
||
order := &billing.Order{
|
||
ID: uuid.New(),
|
||
UserID: userID,
|
||
TargetServerID: targetServerID,
|
||
TariffID: tariffID,
|
||
Amount: selectedTariff.Price,
|
||
Status: billing.StatusPending,
|
||
}
|
||
|
||
if err := s.billingRepo.CreateOrder(order); err != nil {
|
||
return nil, "", err
|
||
}
|
||
|
||
// 4. Запрос к ЮКассе
|
||
description := fmt.Sprintf("Оплата тарифа %s", selectedTariff.Name)
|
||
ykResp, err := s.ykClient.CreatePayment(ctx, decimal.NewFromFloat(order.Amount), description, order.ID, returnURL)
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("yookassa error: %w", err)
|
||
}
|
||
|
||
// Сохраняем payment_id от ЮКассы в наш заказ
|
||
if err := s.billingRepo.UpdateOrderStatus(order.ID, billing.StatusPending, ykResp.ID); err != nil {
|
||
return nil, "", err
|
||
}
|
||
|
||
return order, ykResp.Confirmation.URL, nil
|
||
}
|
||
|
||
// ProcessWebhook — вызывается из HTTP хендлера
|
||
func (s *Service) ProcessWebhook(ctx context.Context, event yookassa.WebhookEvent) error {
|
||
if event.Event != "payment.succeeded" {
|
||
return nil
|
||
}
|
||
|
||
orderIDStr := event.Object.Metadata["order_id"]
|
||
orderID, err := uuid.Parse(orderIDStr)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
order, err := s.billingRepo.GetOrder(orderID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if order.Status == billing.StatusPaid {
|
||
return nil
|
||
}
|
||
|
||
return s.applyOrderToBalance(order, event.Object.ID)
|
||
}
|
||
|
||
// applyOrderToBalance — внутренняя логика начисления (DRY)
|
||
func (s *Service) applyOrderToBalance(order *billing.Order, externalID string) error {
|
||
var tariff *billing.Tariff
|
||
for _, t := range AvailableTariffs {
|
||
if t.ID == order.TariffID {
|
||
tariff = &t
|
||
break
|
||
}
|
||
}
|
||
|
||
server, err := s.accountRepo.GetServerByID(order.TargetServerID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
now := time.Now()
|
||
var newPaidUntil time.Time
|
||
if server.PaidUntil != nil && server.PaidUntil.After(now) {
|
||
newPaidUntil = server.PaidUntil.AddDate(0, 0, tariff.DurationDays)
|
||
} else {
|
||
newPaidUntil = now.AddDate(0, 0, tariff.DurationDays)
|
||
}
|
||
|
||
if err := s.accountRepo.UpdateBalance(server.ID, tariff.InvoicesCount, &newPaidUntil); err != nil {
|
||
return err
|
||
}
|
||
|
||
if s.notifier != nil {
|
||
// Мы запускаем в горутине, чтобы не тормозить ответ ЮКассе
|
||
go s.notifier.NotifySuccess(order.UserID, order.Amount, server.Balance+tariff.InvoicesCount, server.Name)
|
||
}
|
||
|
||
return s.billingRepo.UpdateOrderStatus(order.ID, billing.StatusPaid, externalID)
|
||
}
|
||
|
||
// ConfirmOrder — оставляем для совместимости или ручного подтверждения (если нужно)
|
||
func (s *Service) ConfirmOrder(orderID uuid.UUID) error {
|
||
order, err := s.billingRepo.GetOrder(orderID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return s.applyOrderToBalance(order, "manual_confirmation")
|
||
}
|
||
|
||
// CanProcessInvoice проверяет возможность отправки накладной
|
||
func (s *Service) CanProcessInvoice(serverID uuid.UUID) (bool, error) {
|
||
server, err := s.accountRepo.GetServerByID(serverID)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
if server.Balance <= 0 {
|
||
return false, errors.New("недостаточно накладных на балансе")
|
||
}
|
||
|
||
if server.PaidUntil == nil || server.PaidUntil.Before(time.Now()) {
|
||
return false, errors.New("срок действия услуг истек")
|
||
}
|
||
|
||
return true, nil
|
||
}
|