Files
enkly-ai/services/billing/internal/interfaces/grpc/server.go
enkly 79ce8dc4c0
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s
Init Project
2026-10-30 18:06:55 +01:00

142 lines
4.9 KiB
Go

package grpc
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
billingv1 "enkly/gen/billing/v1"
"enkly/pkg/grpcerr"
"enkly/services/billing/internal/application"
"enkly/services/billing/internal/domain"
)
type Server struct {
billingv1.UnimplementedBillingServiceServer
Svc *application.Service
PublishableKey string // STRIPE_PUBLISH_KEY, for GetPlanCatalogResponse
}
func New(svc *application.Service, publishableKey string) *Server {
return &Server{Svc: svc, PublishableKey: publishableKey}
}
func toProtoPlan(p domain.Plan) billingv1.Plan {
switch p {
case domain.PlanFree:
return billingv1.Plan_PLAN_FREE
case domain.PlanBasic:
return billingv1.Plan_PLAN_BASIC
case domain.PlanPro:
return billingv1.Plan_PLAN_PRO
case domain.PlanPremium:
return billingv1.Plan_PLAN_PREMIUM
default:
return billingv1.Plan_PLAN_UNSPECIFIED
}
}
func fromProtoPlan(p billingv1.Plan) domain.Plan {
switch p {
case billingv1.Plan_PLAN_FREE:
return domain.PlanFree
case billingv1.Plan_PLAN_BASIC:
return domain.PlanBasic
case billingv1.Plan_PLAN_PRO:
return domain.PlanPro
case billingv1.Plan_PLAN_PREMIUM:
return domain.PlanPremium
default:
return domain.PlanFree
}
}
func toProtoSubscription(s *domain.Subscription) *billingv1.Subscription {
out := &billingv1.Subscription{
UserId: s.UserID,
Plan: toProtoPlan(domain.Plan(s.Plan)),
Status: s.Status,
CancelAtPeriodEnd: s.CancelAtPeriodEnd,
HasBillingAccount: s.StripeCustomerID != "",
}
if !s.CurrentPeriodEnd.IsZero() {
out.CurrentPeriodEnd = timestamppb.New(s.CurrentPeriodEnd)
}
return out
}
func (s *Server) GetOrCreateSubscription(ctx context.Context, req *billingv1.GetOrCreateSubscriptionRequest) (*billingv1.Subscription, error) {
sub, err := s.Svc.GetOrCreateSubscription(ctx, req.GetUserId())
if err != nil {
return nil, grpcerr.Internal(err)
}
return toProtoSubscription(sub), nil
}
func (s *Server) GetPlanCatalog(ctx context.Context, req *billingv1.GetPlanCatalogRequest) (*billingv1.GetPlanCatalogResponse, error) {
entries := s.Svc.GetPlanCatalog(ctx)
resp := &billingv1.GetPlanCatalogResponse{PublishableKey: s.PublishableKey}
for _, e := range entries {
resp.Plans = append(resp.Plans, &billingv1.PlanInfo{
Plan: toProtoPlan(e.Plan),
Name: e.Name,
PriceEur: int32(e.PriceEUR),
DailyMessageLimit: int32(e.DailyMessageLimit),
Available: e.Available,
})
}
return resp, nil
}
func (s *Server) CreateCheckoutSession(ctx context.Context, req *billingv1.CreateCheckoutSessionRequest) (*billingv1.CreateCheckoutSessionResponse, error) {
url, err := s.Svc.CreateCheckoutSession(ctx, req.GetUserId(), req.GetEmail(), fromProtoPlan(req.GetPlan()), req.GetSuccessUrl(), req.GetCancelUrl())
if err != nil {
return nil, grpcerr.FailedPrecondition(err.Error())
}
return &billingv1.CreateCheckoutSessionResponse{CheckoutUrl: url}, nil
}
func (s *Server) SyncCheckoutSession(ctx context.Context, req *billingv1.SyncCheckoutSessionRequest) (*billingv1.Subscription, error) {
sub, err := s.Svc.SyncCheckoutSession(ctx, req.GetUserId(), req.GetStripeSessionId())
if err != nil {
return nil, grpcerr.FailedPrecondition(err.Error())
}
return toProtoSubscription(sub), nil
}
func (s *Server) CreateBillingPortalSession(ctx context.Context, req *billingv1.CreateBillingPortalSessionRequest) (*billingv1.CreateBillingPortalSessionResponse, error) {
url, err := s.Svc.CreateBillingPortalSession(ctx, req.GetUserId(), req.GetReturnUrl())
if err != nil {
if application.IsNoBillingAccount(err) {
return nil, grpcerr.FailedPrecondition("no billing account yet — subscribe to a paid plan first")
}
return nil, grpcerr.FailedPrecondition(err.Error())
}
return &billingv1.CreateBillingPortalSessionResponse{PortalUrl: url}, nil
}
func (s *Server) IngestStripeWebhook(ctx context.Context, req *billingv1.IngestStripeWebhookRequest) (*billingv1.IngestStripeWebhookResponse, error) {
if err := s.Svc.IngestStripeWebhook(ctx, req.GetPayload(), req.GetSignatureHeader()); err != nil {
return nil, grpcerr.InvalidArgument(err.Error())
}
return &billingv1.IngestStripeWebhookResponse{Accepted: true}, nil
}
func (s *Server) CheckAndIncrementUsage(ctx context.Context, req *billingv1.CheckAndIncrementUsageRequest) (*billingv1.CheckAndIncrementUsageResponse, error) {
allowed, used, limit, planName, err := s.Svc.CheckAndIncrementUsage(ctx, req.GetUserId())
if err != nil {
return nil, grpcerr.Internal(err)
}
return &billingv1.CheckAndIncrementUsageResponse{
Allowed: allowed, Used: int32(used), Limit: int32(limit), PlanName: planName,
}, nil
}
func (s *Server) GetDailyUsage(ctx context.Context, req *billingv1.GetDailyUsageRequest) (*billingv1.GetDailyUsageResponse, error) {
used, err := s.Svc.GetDailyUsage(ctx, req.GetUserId())
if err != nil {
return nil, grpcerr.Internal(err)
}
return &billingv1.GetDailyUsageResponse{Used: int32(used)}, nil
}