mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
добавлен биллинг и тарифы
добавлена интеграция с юкасса
This commit is contained in:
@@ -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{},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
47
internal/infrastructure/repository/billing/postgres.go
Normal file
47
internal/infrastructure/repository/billing/postgres.go
Normal 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
|
||||
}
|
||||
79
internal/infrastructure/yookassa/client.go
Normal file
79
internal/infrastructure/yookassa/client.go
Normal 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
|
||||
}
|
||||
34
internal/infrastructure/yookassa/dto.go
Normal file
34
internal/infrastructure/yookassa/dto.go
Normal 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"`
|
||||
}
|
||||
Reference in New Issue
Block a user