the-moon-up-above-03-api

package sales import ( "context" "fmt" "time" "github.com/google/uuid" "github.com/jayfreestone/the-moon-up-above-04/internal/conversion" "github.com/jayfreestone/the-moon-up-above-04/internal/validation" "github.com/pkg/errors" ) type Service struct { repo Repository productRepo ProductRepository paymentProvider PaymentProvider // At some point this will be a map so we can pick per request } func NewService(repo Repository, productRepo ProductRepository, paymentProvider PaymentProvider) *Service { return &Service{ repo: repo, productRepo: productRepo, paymentProvider: paymentProvider, } } func (s *Service) GetBasket(wishlist Wishlist) (*Basket, error) { if err := wishlist.Validate(); err != nil { return nil, err } ids := wishlist.IDs() products, err := s.productRepo.GetByIDs(ids) if err != nil { return nil, errors.Wrap(err, "unable to fetch products for wishlist") } basket, err := wishlist.FulfillWith(products) if err != nil { return nil, errors.Wrap(err, "unable to create basket from wishlist") } return &basket, nil } // StartCheckout creates a new pending order and payment transaction. func (s *Service) StartCheckout(newOrder OrderRequest) (*Checkout, error) { if err := newOrder.Validate(); err != nil { return nil, err } // First we recreate the basket from the wishlist, confirming it's valid basket, err := s.GetBasket(*newOrder.Wishlist) if err != nil { return nil, errors.Wrap(err, "unable to create basket from wishlist") } // Double check that the client-provided total matches up with what we were expecting. // We only compare the totals, since the client won't be overly concerned if price allocation // is different. The user will have another chance to see the price before payment, but // this early check gives us a little peace of mind that the user has already seen the total. if basket.Total() != *newOrder.ExpectedTotal { return nil, &validation.Error{ Message: "Invalid order request", Errors: []validation.InputError{ { Field: conversion.String("Total"), Message: fmt.Sprintf("expected total (%v) did not match actual (%v)", basket.Total().Amount, newOrder.ExpectedTotal.Amount), }, }, } } order := NewOrderFromBasket(*basket) transaction, err := s.paymentProvider.CreateTransaction(context.TODO(), order) if err != nil { return nil, errors.Wrap(err, "unable to create transaction for order") } if err := s.repo.Create(*order); err != nil { return nil, errors.Wrap(err, "unable to create new order") } return &Checkout{ Order: order, Transaction: transaction, }, err } // CompleteCheckout marks the payment process as completed, transitioning the order's status and updating inventory. // Note that stock updates and order updates are *not* committed as one atomic operation, meaning it is possible // to end up in an inconsistent state in the unlikely event that one DB update succeeds and another does not. // Handling this would introduce a lot of complexity for (presumably) a rare scenario, hence it is left as an // understood risk rather than an oversight. func (s *Service) CompleteCheckout(orderID uuid.UUID) error { now := time.Now() order, err := s.GetOrderByID(orderID) if err != nil { return errors.Wrapf(err, "unable to get order %s in order to complete checkout", orderID) } order.MarkAsPaid() order.MarkModified(now) orderProducts, err := s.productRepo.GetByIDs(order.LineItems.ProductIDs()) if err != nil { return errors.Wrap(err, "unable to find products during checkout completion") } if err := order.LineItems.DeductInventory(orderProducts); err != nil { return errors.Wrap(err, "unable to deduct product inventory during checkout completion") } orderProducts.MarkModified(now) if err := s.productRepo.UpdateProducts(orderProducts); err != nil { return errors.Wrapf(err, "unable to update products for order with id %s", orderID) } if err := s.repo.Update(*order); err != nil { return errors.Wrapf(err, "unable to complete checkout for order with id %s", orderID) } return nil } func (s *Service) GetOrderByID(orderID uuid.UUID) (*Order, error) { order, err := s.repo.GetByID(orderID) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("unable to find order with id %s", orderID)) } return order, nil }