добавлен биллинг и тарифы

добавлена интеграция с юкасса
This commit is contained in:
2025-12-24 09:06:19 +03:00
parent b4ce819931
commit 5f35d7a75f
15 changed files with 745 additions and 212 deletions

View File

@@ -7,6 +7,7 @@ import (
"os"
"regexp"
"rmser/internal/domain/account"
"rmser/internal/domain/billing"
"rmser/internal/domain/catalog"
"rmser/internal/domain/drafts"
"rmser/internal/domain/invoices"
@@ -51,6 +52,7 @@ func NewPostgresDB(dsn string) *gorm.DB {
&account.User{},
&account.RMSServer{},
&account.ServerUser{},
&billing.Order{},
&catalog.Product{},
&catalog.MeasureUnit{},
&catalog.ProductContainer{},

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"rmser/internal/domain/account"
@@ -60,6 +61,15 @@ func (r *pgRepository) GetUserByTelegramID(telegramID int64) (*account.User, err
return &user, nil
}
func (r *pgRepository) GetUserByID(id uuid.UUID) (*account.User, error) {
var user account.User
err := r.db.Where("id = ?", id).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
// ConnectServer - Основная точка входа для добавления сервера
func (r *pgRepository) ConnectServer(userID uuid.UUID, rawURL, login, encryptedPass, name string) (*account.RMSServer, error) {
// 1. Нормализация URL (удаляем слеш в конце, приводим к нижнему регистру)
@@ -401,3 +411,37 @@ func (r *pgRepository) GetConnectionByID(id uuid.UUID) (*account.ServerUser, err
}
return &link, nil
}
func (r *pgRepository) GetServerByURL(rawURL string) (*account.RMSServer, error) {
cleanURL := strings.TrimRight(strings.ToLower(strings.TrimSpace(rawURL)), "/")
var server account.RMSServer
err := r.db.Where("base_url = ?", cleanURL).First(&server).Error
if err != nil {
return nil, err
}
return &server, nil
}
func (r *pgRepository) GetServerByID(id uuid.UUID) (*account.RMSServer, error) {
var server account.RMSServer
err := r.db.First(&server, id).Error
if err != nil {
return nil, err
}
return &server, nil
}
// UpdateBalance начисляет пакет или продлевает подписку
func (r *pgRepository) UpdateBalance(serverID uuid.UUID, amountChange int, newPaidUntil *time.Time) error {
return r.db.Model(&account.RMSServer{}).Where("id = ?", serverID).Updates(map[string]interface{}{
"balance": gorm.Expr("balance + ?", amountChange),
"paid_until": newPaidUntil,
}).Error
}
// DecrementBalance списывает 1 единицу при отправке накладной
func (r *pgRepository) DecrementBalance(serverID uuid.UUID) error {
return r.db.Model(&account.RMSServer{}).
Where("id = ? AND balance > 0", serverID).
UpdateColumn("balance", gorm.Expr("balance - ?", 1)).Error
}

View File

@@ -0,0 +1,47 @@
package billing
import (
"rmser/internal/domain/billing"
"github.com/google/uuid"
"gorm.io/gorm"
)
type pgRepository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) billing.Repository {
return &pgRepository{db: db}
}
func (r *pgRepository) CreateOrder(order *billing.Order) error {
return r.db.Create(order).Error
}
func (r *pgRepository) GetOrder(id uuid.UUID) (*billing.Order, error) {
var order billing.Order
err := r.db.Where("id = ?", id).First(&order).Error
if err != nil {
return nil, err
}
return &order, nil
}
func (r *pgRepository) UpdateOrderStatus(id uuid.UUID, status billing.OrderStatus, paymentID string) error {
updates := map[string]interface{}{
"status": status,
}
if paymentID != "" {
updates["payment_id"] = paymentID
}
return r.db.Model(&billing.Order{}).Where("id = ?", id).Updates(updates).Error
}
func (r *pgRepository) GetActiveOrdersByUser(userID uuid.UUID) ([]billing.Order, error) {
var orders []billing.Order
err := r.db.Where("user_id = ? AND status = ?", userID, billing.StatusPending).
Order("created_at DESC").
Find(&orders).Error
return orders, err
}

View File

@@ -0,0 +1,79 @@
package yookassa
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/shopspring/decimal"
)
type Client struct {
shopID string
secretKey string
baseURL string
httpClient *http.Client
}
func NewClient(shopID, secretKey string) *Client {
return &Client{
shopID: shopID,
secretKey: secretKey,
baseURL: "https://api.yookassa.ru/v3",
httpClient: &http.Client{Timeout: 15 * time.Second},
}
}
func (c *Client) CreatePayment(ctx context.Context, amount decimal.Decimal, description string, orderID uuid.UUID, returnURL string) (*PaymentResponse, error) {
reqBody := PaymentRequest{
Amount: Amount{
Value: amount.StringFixed(2),
Currency: "RUB",
},
Capture: true, // Автоматическое списание
Confirmation: Confirmation{
Type: "redirect",
ReturnURL: returnURL,
},
Metadata: map[string]string{
"order_id": orderID.String(),
},
Description: description,
}
body, _ := json.Marshal(reqBody)
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/payments", bytes.NewReader(body))
if err != nil {
return nil, err
}
// Basic Auth
auth := base64.StdEncoding.EncodeToString([]byte(c.shopID + ":" + c.secretKey))
req.Header.Set("Authorization", "Basic "+auth)
req.Header.Set("Content-Type", "application/json")
// Идемпотентность: используем наш order_id как ключ
req.Header.Set("Idempotence-Key", orderID.String())
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("yookassa error: status %d", resp.StatusCode)
}
var result PaymentResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}

View File

@@ -0,0 +1,34 @@
package yookassa
type Amount struct {
Value string `json:"value"`
Currency string `json:"currency"`
}
type Confirmation struct {
Type string `json:"type"`
ReturnURL string `json:"return_url,omitempty"`
URL string `json:"confirmation_url,omitempty"`
}
type PaymentRequest struct {
Amount Amount `json:"amount"`
Capture bool `json:"capture"`
Confirmation Confirmation `json:"confirmation"`
Metadata map[string]string `json:"metadata"`
Description string `json:"description"`
}
type PaymentResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Amount Amount `json:"amount"`
Confirmation Confirmation `json:"confirmation"`
Metadata map[string]string `json:"metadata"`
}
type WebhookEvent struct {
Event string `json:"event"`
Type string `json:"type"`
Object PaymentResponse `json:"object"`
}