package quotation import ( "context" "errors" "strings" "github.com/tech/sendico/payments/storage" "github.com/tech/sendico/payments/storage/model" "github.com/tech/sendico/pkg/merrors" quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1" sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/protobuf/proto" ) func validateMetaAndOrgRef(meta *sharedv1.RequestMeta) (string, bson.ObjectID, error) { if meta == nil { return "", bson.NilObjectID, merrors.InvalidArgument("meta is required") } orgRef := strings.TrimSpace(meta.GetOrganizationRef()) if orgRef == "" { return "", bson.NilObjectID, merrors.InvalidArgument("organization_ref is required") } orgID, err := bson.ObjectIDFromHex(orgRef) if err != nil { return "", bson.NilObjectID, merrors.InvalidArgument("organization_ref must be a valid objectID") } return orgRef, orgID, nil } func requireNonNilIntent(intent *sharedv1.PaymentIntent) error { if intent == nil { return merrors.InvalidArgument("intent is required") } if intent.GetAmount() == nil { return merrors.InvalidArgument("intent.amount is required") } if strings.TrimSpace(intent.GetSettlementCurrency()) == "" { return merrors.InvalidArgument("intent.settlement_currency is required") } return nil } func ensureQuotesStore(repo storage.Repository) (storage.QuotesStore, error) { if repo == nil { return nil, errStorageUnavailable } store := repo.Quotes() if store == nil { return nil, errStorageUnavailable } return store, nil } type quoteResolutionInput struct { OrgRef string OrgID bson.ObjectID Meta *sharedv1.RequestMeta Intent *sharedv1.PaymentIntent QuoteRef string IdempotencyKey string } type quoteResolutionError struct { code string err error } func (e quoteResolutionError) Error() string { return e.err.Error() } func (s *Service) resolvePaymentQuote(ctx context.Context, in quoteResolutionInput) (*sharedv1.PaymentQuote, *sharedv1.PaymentIntent, *model.PaymentPlan, error) { if ref := strings.TrimSpace(in.QuoteRef); ref != "" { quotesStore, err := ensureQuotesStore(s.storage) if err != nil { return nil, nil, nil, err } record, err := quotesStore.GetByRef(ctx, in.OrgID, ref) if err != nil { if errors.Is(err, storage.ErrQuoteNotFound) { return nil, nil, nil, quoteResolutionError{code: "quote_not_found", err: merrors.InvalidArgument("quote_ref not found or expired")} } return nil, nil, nil, err } if !record.ExpiresAt.IsZero() && s.clock.Now().After(record.ExpiresAt) { return nil, nil, nil, quoteResolutionError{code: "quote_expired", err: merrors.InvalidArgument("quote_ref expired")} } intent, err := recordIntentFromQuote(record) if err != nil { return nil, nil, nil, err } if in.Intent != nil && !proto.Equal(intent, in.Intent) { return nil, nil, nil, quoteResolutionError{code: "quote_intent_mismatch", err: merrors.InvalidArgument("quote_ref does not match intent")} } quote, err := recordQuoteFromQuote(record) if err != nil { return nil, nil, nil, err } quote.QuoteRef = ref plan, err := recordPlanFromQuote(record) if err != nil { return nil, nil, nil, err } return quote, intent, plan, nil } if in.Intent == nil { return nil, nil, nil, merrors.InvalidArgument("intent is required") } req := "ationv1.QuotePaymentRequest{ Meta: in.Meta, IdempotencyKey: in.IdempotencyKey, Intent: in.Intent, PreviewOnly: false, } quote, _, err := s.buildPaymentQuote(ctx, in.OrgRef, req) if err != nil { return nil, nil, nil, err } plan, err := s.buildPaymentPlan(ctx, in.OrgID, in.Intent, in.IdempotencyKey, quote) if err != nil { return nil, nil, nil, err } return quote, in.Intent, plan, nil } func recordIntentFromQuote(record *model.PaymentQuoteRecord) (*sharedv1.PaymentIntent, error) { if record == nil { return nil, merrors.InvalidArgument("stored quote payload is incomplete") } if len(record.Intents) > 0 { if len(record.Intents) != 1 { return nil, merrors.InvalidArgument("stored quote payload is incomplete") } return protoIntentFromModel(record.Intents[0]), nil } if record.Intent.Amount == nil && (record.Intent.Kind == "" || record.Intent.Kind == model.PaymentKindUnspecified) { return nil, merrors.InvalidArgument("stored quote payload is incomplete") } return protoIntentFromModel(record.Intent), nil } func recordQuoteFromQuote(record *model.PaymentQuoteRecord) (*sharedv1.PaymentQuote, error) { if record == nil { return nil, merrors.InvalidArgument("stored quote is empty") } if record.Quote != nil { return modelQuoteToProto(record.Quote), nil } if len(record.Quotes) > 0 { if len(record.Quotes) != 1 { return nil, merrors.InvalidArgument("stored quote payload is incomplete") } return modelQuoteToProto(record.Quotes[0]), nil } return nil, merrors.InvalidArgument("stored quote is empty") } func recordPlanFromQuote(record *model.PaymentQuoteRecord) (*model.PaymentPlan, error) { if record == nil { return nil, merrors.InvalidArgument("stored quote payload is incomplete") } if len(record.Plans) > 0 { if len(record.Plans) != 1 { return nil, merrors.InvalidArgument("stored quote payload is incomplete") } return cloneStoredPaymentPlan(record.Plans[0]), nil } if record.Plan != nil { return cloneStoredPaymentPlan(record.Plan), nil } return nil, nil } func newPayment(orgID bson.ObjectID, intent *sharedv1.PaymentIntent, idempotencyKey string, metadata map[string]string, quote *sharedv1.PaymentQuote) *model.Payment { entity := &model.Payment{} entity.SetID(bson.NewObjectID()) entity.SetOrganizationRef(orgID) entity.PaymentRef = entity.GetID().Hex() entity.IdempotencyKey = idempotencyKey entity.State = model.PaymentStateAccepted entity.Intent = intentFromProto(intent) entity.Metadata = cloneMetadata(metadata) entity.LastQuote = quoteSnapshotToModel(quote) entity.Normalize() return entity }