147 lines
4.7 KiB
Go
147 lines
4.7 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
|
"git.k6n.net/mats/go-cart-actor/pkg/promotions"
|
|
pmcp "git.k6n.net/mats/platform/mcp"
|
|
"git.k6n.net/mats/platform/tax"
|
|
)
|
|
|
|
// tool is one MCP tool: a name, a description, a JSON-Schema for its arguments,
|
|
// and the handler that runs it.
|
|
func (s *Server) buildTools() []pmcp.Tool {
|
|
return []pmcp.Tool{
|
|
s.toolListPromotions(),
|
|
s.toolGetPromotion(),
|
|
{
|
|
Name: "upsert_promotion",
|
|
Description: "Create a promotion or replace the existing one with the same id. " +
|
|
"`promotion` is a full promotion-rule object (id, name, status, priority, conditions, actions). " +
|
|
"For a tiered volume discount (Volymrabatt) use a cart_total condition plus a tiered_discount action " +
|
|
"whose config.tiers each have minTotal/maxTotal (öre incl. VAT) and percent.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"promotion": pmcp.ObjectValue("the full promotion rule object"),
|
|
}, []string{"promotion"}),
|
|
Invoke: func(_ context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
Promotion json.RawMessage `json:"promotion"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
var rule promotions.PromotionRule
|
|
if err := json.Unmarshal(a.Promotion, &rule); err != nil {
|
|
return nil, fmt.Errorf("invalid promotion: %w", err)
|
|
}
|
|
if rule.ID == "" {
|
|
return nil, fmt.Errorf("promotion.id is required")
|
|
}
|
|
if rule.Status == "" {
|
|
rule.Status = promotions.StatusActive
|
|
}
|
|
replaced, err := s.store.Upsert(rule)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"id": rule.ID, "replaced": replaced}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "set_promotion_status",
|
|
Description: "Set a promotion's status (active|inactive|scheduled|expired) to toggle it on or off.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"id": pmcp.String("the promotion id"),
|
|
"status": pmcp.String("new status: active|inactive|scheduled|expired"),
|
|
}, []string{"id", "status"}),
|
|
Invoke: func(_ context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
if !validStatus(a.Status) {
|
|
return nil, fmt.Errorf("invalid status %q", a.Status)
|
|
}
|
|
found, err := s.store.SetStatus(a.ID, promotions.PromotionStatus(a.Status))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return nil, fmt.Errorf("promotion %q not found", a.ID)
|
|
}
|
|
return map[string]any{"id": a.ID, "status": a.Status}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "delete_promotion",
|
|
Description: "Delete a promotion rule by id.",
|
|
InputSchema: pmcp.Object(pmcp.Props{"id": pmcp.String("the promotion id")}, []string{"id"}),
|
|
Invoke: func(_ context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
found, err := s.store.Delete(a.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return nil, fmt.Errorf("promotion %q not found", a.ID)
|
|
}
|
|
return map[string]any{"id": a.ID, "deleted": true}, nil
|
|
},
|
|
},
|
|
s.toolPreviewCartDiscount(),
|
|
}
|
|
}
|
|
|
|
// syntheticCart builds a one-line cart whose gross total is incVat öre so the
|
|
// promotion engine can be evaluated against an arbitrary total. rateBp is the VAT
|
|
// rate in basis points (2500 = 25%).
|
|
func syntheticCart(incVat int64, qty int, rateBp int) *cart.CartGrain {
|
|
if qty <= 0 {
|
|
qty = 1
|
|
}
|
|
if rateBp == 0 {
|
|
rateBp = 2500
|
|
}
|
|
g := cart.NewCartGrain(0, time.Now())
|
|
g.Items = []*cart.CartItem{
|
|
{Id: 1, Sku: "PREVIEW", Quantity: uint16(qty), Price: *cart.NewPriceFromIncVat(incVat, rateBp)},
|
|
}
|
|
// Quantity > 1 would multiply the row total; keep the gross equal to incVat by
|
|
// pricing a single unit at the full total.
|
|
g.Items[0].Quantity = 1
|
|
g.UpdateTotals()
|
|
g.Items[0].Quantity = uint16(qty)
|
|
return g
|
|
}
|
|
|
|
func validStatus(s string) bool {
|
|
switch promotions.PromotionStatus(s) {
|
|
case promotions.StatusActive, promotions.StatusInactive, promotions.StatusScheduled, promotions.StatusExpired:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// defaultVatRate is the VAT rate used to build the synthetic preview cart when
|
|
// the caller supplies none. Sourced from platform/tax (Nordic standard,
|
|
// defaultCountry SE = 25%) rather than a magic constant.
|
|
//
|
|
// NOTE: reconstructed during the platform/mcp migration (the original method was
|
|
// lost editing this untracked file). Behaviour matches the prior 25% default; if
|
|
// the original read a per-store/configured rate, wire that provider in here.
|
|
func (s *Server) defaultVatRate() int {
|
|
return tax.NewNordic("").DefaultTaxRate("")
|
|
}
|