all the refactor
Build and Publish / BuildAndDeployAmd64 (push) Failing after 5s
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s

This commit is contained in:
2026-06-28 17:51:52 +02:00
parent 7db0d236c7
commit aa8b2bdedc
84 changed files with 4328 additions and 2344 deletions
+10 -9
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"git.k6n.net/mats/go-cart-actor/pkg/promotions"
pmcp "git.k6n.net/mats/platform/mcp"
)
// AdminTool is one admin promotion tool definition — name, description, input
@@ -31,14 +32,14 @@ func AdminTools(store *promotions.Store, eval *promotions.PromotionService) []Ad
"`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: object(props{
"promotion": obj("the full promotion rule object"),
InputSchema: pmcp.Object(pmcp.Props{
"promotion": pmcp.ObjectValue("the full promotion rule object"),
}, []string{"promotion"}),
Invoke: func(args json.RawMessage) (any, error) {
var a struct {
Promotion json.RawMessage `json:"promotion"`
}
if err := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
var rule promotions.PromotionRule
@@ -61,16 +62,16 @@ func AdminTools(store *promotions.Store, eval *promotions.PromotionService) []Ad
{
Name: "set_promotion_status",
Description: "Set a promotion's status (active|inactive|scheduled|expired) to toggle it on or off.",
InputSchema: object(props{
"id": str("the promotion id"),
"status": str("new status: active|inactive|scheduled|expired"),
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(args json.RawMessage) (any, error) {
var a struct {
ID string `json:"id"`
Status string `json:"status"`
}
if err := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
if !validStatus(a.Status) {
@@ -89,12 +90,12 @@ func AdminTools(store *promotions.Store, eval *promotions.PromotionService) []Ad
{
Name: "delete_promotion",
Description: "Delete a promotion rule by id.",
InputSchema: object(props{"id": str("the promotion id")}, []string{"id"}),
InputSchema: pmcp.Object(pmcp.Props{"id": pmcp.String("the promotion id")}, []string{"id"}),
Invoke: func(args json.RawMessage) (any, error) {
var a struct {
ID string `json:"id"`
}
if err := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
found, err := store.Delete(a.ID)
-44
View File
@@ -1,44 +0,0 @@
package mcp
import "encoding/json"
// JSON-RPC 2.0 envelope types (the wire format MCP rides on). Notifications are
// requests with no id and must not receive a response.
type rpcRequest struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
func (r *rpcRequest) isNotification() bool { return len(r.ID) == 0 }
type rpcResponse struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id,omitempty"`
Result any `json:"result,omitempty"`
Error *rpcError `json:"error,omitempty"`
}
type rpcError struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
const (
codeParseError = -32700
codeInvalidRequest = -32600
codeMethodNotFound = -32601
codeInvalidParams = -32602
codeInternalError = -32603
)
func result(id json.RawMessage, v any) *rpcResponse {
return &rpcResponse{JSONRPC: "2.0", ID: id, Result: v}
}
func errorResponse(id json.RawMessage, code int, msg string) *rpcResponse {
return &rpcResponse{JSONRPC: "2.0", ID: id, Error: &rpcError{Code: code, Message: msg}}
}
+10 -131
View File
@@ -4,160 +4,39 @@
// cart actually applies (e.g. Volymrabatt), plus preview the discount a cart
// total would receive.
//
// Transport is streamable HTTP (JSON-RPC 2.0 over POST). It is mounted by
// cmd/cart under /mcp on the same HTTP server as the cart API. Tools mutate the
// shared promotions.Store and persist to data/promotions.json; the cart's
// mutation processor reads a fresh Store snapshot on every recompute, so edits
// take effect on the next cart change without a restart.
// Transport/dispatch and the schema/result helpers live in platform/mcp; this
// package defines the promotion tools (full set via New, read-only via NewPublic)
// and the admin tools (AdminTools) that backoffice merges into the CMS MCP.
package mcp
import (
"encoding/json"
"io"
"net/http"
"git.k6n.net/mats/go-cart-actor/pkg/promotions"
pmcp "git.k6n.net/mats/platform/mcp"
)
const (
serverName = "cart-promotions"
serverVersion = "0.1.0"
protocolVersion = "2025-06-18"
serverName = "cart-promotions"
serverVersion = "0.1.0"
)
// Server is the MCP edge over the shared promotion Store and evaluator.
type Server struct {
store *promotions.Store
eval *promotions.PromotionService
tools []tool
mcp *pmcp.Server
}
// New builds an MCP server exposing the promotion store as tools.
// New builds an MCP server exposing the full promotion tool set.
func New(store *promotions.Store, eval *promotions.PromotionService) *Server {
if eval == nil {
eval = promotions.NewPromotionService(nil)
}
s := &Server{store: store, eval: eval}
s.tools = s.buildTools()
s.mcp = pmcp.NewServer(pmcp.Info{Name: serverName, Version: serverVersion}, s.buildTools()...)
return s
}
// Handler returns the streamable-HTTP handler to mount (e.g. at /mcp).
func (s *Server) Handler() http.Handler {
return http.HandlerFunc(s.serveHTTP)
}
func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", "POST")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20))
if err != nil {
writeRPC(w, errorResponse(nil, codeParseError, "read error"))
return
}
if isBatch(body) {
var reqs []rpcRequest
if err := json.Unmarshal(body, &reqs); err != nil {
writeRPC(w, errorResponse(nil, codeParseError, "invalid JSON"))
return
}
var out []*rpcResponse
for i := range reqs {
if resp := s.dispatch(&reqs[i]); resp != nil {
out = append(out, resp)
}
}
if len(out) == 0 {
w.WriteHeader(http.StatusAccepted)
return
}
writeRPC(w, out)
return
}
var req rpcRequest
if err := json.Unmarshal(body, &req); err != nil {
writeRPC(w, errorResponse(nil, codeParseError, "invalid JSON"))
return
}
resp := s.dispatch(&req)
if resp == nil {
w.WriteHeader(http.StatusAccepted)
return
}
writeRPC(w, resp)
}
func (s *Server) dispatch(req *rpcRequest) *rpcResponse {
if req.JSONRPC != "2.0" {
if req.isNotification() {
return nil
}
return errorResponse(req.ID, codeInvalidRequest, "jsonrpc must be \"2.0\"")
}
switch req.Method {
case "initialize":
return result(req.ID, s.initialize(req.Params))
case "ping":
return result(req.ID, struct{}{})
case "tools/list":
return result(req.ID, map[string]any{"tools": s.tools})
case "tools/call":
return s.callTool(req)
case "notifications/initialized", "notifications/cancelled":
return nil
default:
if req.isNotification() {
return nil
}
return errorResponse(req.ID, codeMethodNotFound, "unknown method: "+req.Method)
}
}
func (s *Server) initialize(params json.RawMessage) map[string]any {
pv := protocolVersion
if len(params) > 0 {
var p struct {
ProtocolVersion string `json:"protocolVersion"`
}
if json.Unmarshal(params, &p) == nil && p.ProtocolVersion != "" {
pv = p.ProtocolVersion
}
}
return map[string]any{
"protocolVersion": pv,
"capabilities": map[string]any{"tools": map[string]any{}},
"serverInfo": map[string]any{"name": serverName, "version": serverVersion},
}
}
// defaultVatRate returns the default VAT rate from the eval service's
// configured tax provider, falling back to 25 if none is set.
func (s *Server) defaultVatRate() float32 {
if s.eval == nil || s.eval.DefaultTaxProvider == nil {
return 25
}
return float32(s.eval.DefaultTaxProvider.DefaultTaxRate(""))
}
func isBatch(body []byte) bool {
for _, b := range body {
switch b {
case ' ', '\t', '\r', '\n':
continue
case '[':
return true
default:
return false
}
}
return false
}
func writeRPC(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(v)
}
func (s *Server) Handler() http.Handler { return s.mcp.Handler() }
+29 -27
View File
@@ -1,11 +1,13 @@
package mcp
import (
"context"
"encoding/json"
"fmt"
"time"
"git.k6n.net/mats/go-cart-actor/pkg/promotions"
pmcp "git.k6n.net/mats/platform/mcp"
)
// NewPublic builds an MCP server exposing only the read-only promotion tools:
@@ -17,23 +19,23 @@ func NewPublic(store *promotions.Store, eval *promotions.PromotionService) *Serv
eval = promotions.NewPromotionService(nil)
}
s := &Server{store: store, eval: eval}
s.tools = s.buildPublicTools()
s.mcp = pmcp.NewServer(pmcp.Info{Name: serverName, Version: serverVersion}, s.buildPublicTools()...)
return s
}
func (s *Server) buildPublicTools() []tool {
return []tool{
func (s *Server) buildPublicTools() []pmcp.Tool {
return []pmcp.Tool{
{
Name: "list_promotions",
Description: "List all promotion rules the cart engine evaluates, optionally filtered by status (active|inactive|scheduled|expired).",
InputSchema: object(props{
"status": str("optional status filter: active|inactive|scheduled|expired"),
InputSchema: pmcp.Object(pmcp.Props{
"status": pmcp.String("optional status filter: active|inactive|scheduled|expired"),
}, nil),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(_ context.Context, args json.RawMessage) (any, error) {
var a struct {
Status string `json:"status"`
}
if err := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
rules := s.store.List()
@@ -52,12 +54,12 @@ func (s *Server) buildPublicTools() []tool {
{
Name: "get_promotion",
Description: "Fetch a single promotion rule by id.",
InputSchema: object(props{"id": str("the promotion id")}, []string{"id"}),
invoke: func(args json.RawMessage) (any, error) {
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 := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
r, ok := s.store.Get(a.ID)
@@ -72,18 +74,18 @@ func (s *Server) buildPublicTools() []tool {
Description: "Evaluate the current active promotions against a hypothetical cart total and report the " +
"matched discount. cartTotalIncVat is in öre incl. VAT (e.g. 5 000 000 = 50 000 kr). Useful for " +
"verifying Volymrabatt tiers without touching a real cart.",
InputSchema: object(props{
"cartTotalIncVat": integer("cart total incl. VAT, in öre"),
"itemQuantity": integer("optional total item quantity"),
"customerSegment": str("optional customer segment"),
InputSchema: pmcp.Object(pmcp.Props{
"cartTotalIncVat": pmcp.Integer("cart total incl. VAT, in öre"),
"itemQuantity": pmcp.Integer("optional total item quantity"),
"customerSegment": pmcp.String("optional customer segment"),
}, []string{"cartTotalIncVat"}),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(_ context.Context, args json.RawMessage) (any, error) {
var a struct {
CartTotalIncVat int64 `json:"cartTotalIncVat"`
ItemQuantity int `json:"itemQuantity"`
CustomerSegment string `json:"customerSegment"`
}
if err := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
g := syntheticCart(a.CartTotalIncVat, a.ItemQuantity, s.defaultVatRate())
@@ -109,20 +111,20 @@ func (s *Server) buildPublicTools() []tool {
"which promotions match, what discount they apply, and any pending next-tier nudges (\"spend X more for Y%\"). " +
"When promotionId is set, only that specific promotion is evaluated (useful for testing a new rule). " +
"When omitted, all active promotions are evaluated. cartTotalIncVat is in öre incl. VAT.",
InputSchema: object(props{
"cartTotalIncVat": integer("cart total incl. VAT, in öre"),
"promotionId": str("optional: evaluate only this promotion (by id)"),
"itemQuantity": integer("optional total item quantity"),
"customerSegment": str("optional customer segment"),
InputSchema: pmcp.Object(pmcp.Props{
"cartTotalIncVat": pmcp.Integer("cart total incl. VAT, in öre"),
"promotionId": pmcp.String("optional: evaluate only this promotion (by id)"),
"itemQuantity": pmcp.Integer("optional total item quantity"),
"customerSegment": pmcp.String("optional customer segment"),
}, []string{"cartTotalIncVat"}),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(_ context.Context, args json.RawMessage) (any, error) {
var a struct {
CartTotalIncVat int64 `json:"cartTotalIncVat"`
PromotionId string `json:"promotionId"`
ItemQuantity int `json:"itemQuantity"`
CustomerSegment string `json:"customerSegment"`
}
if err := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
@@ -151,10 +153,10 @@ func (s *Server) buildPublicTools() []tool {
evalResults := make([]map[string]any, 0, len(results))
for _, r := range results {
evalResults = append(evalResults, map[string]any{
"ruleId": r.Rule.ID,
"name": r.Rule.Name,
"applicable": r.Applicable,
"failedReason": r.FailedReason,
"ruleId": r.Rule.ID,
"name": r.Rule.Name,
"applicable": r.Applicable,
"failedReason": r.FailedReason,
"matchedActions": r.MatchedActions,
})
}
+46 -117
View File
@@ -1,61 +1,32 @@
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.
type tool struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema json.RawMessage `json:"inputSchema"`
invoke func(args json.RawMessage) (any, error) `json:"-"`
}
func (s *Server) callTool(req *rpcRequest) *rpcResponse {
var p struct {
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
}
if err := json.Unmarshal(req.Params, &p); err != nil {
return errorResponse(req.ID, codeInvalidParams, "invalid params")
}
var t *tool
for i := range s.tools {
if s.tools[i].Name == p.Name {
t = &s.tools[i]
break
}
}
if t == nil {
return errorResponse(req.ID, codeInvalidParams, "unknown tool: "+p.Name)
}
out, err := t.invoke(p.Arguments)
if err != nil {
return result(req.ID, toolError(err))
}
return result(req.ID, toolText(out))
}
func (s *Server) buildTools() []tool {
return []tool{
func (s *Server) buildTools() []pmcp.Tool {
return []pmcp.Tool{
{
Name: "list_promotions",
Description: "List all promotion rules the cart engine evaluates, optionally filtered by status (active|inactive|scheduled|expired).",
InputSchema: object(props{
"status": str("optional status filter: active|inactive|scheduled|expired"),
InputSchema: pmcp.Object(pmcp.Props{
"status": pmcp.String("optional status filter: active|inactive|scheduled|expired"),
}, nil),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(_ context.Context, args json.RawMessage) (any, error) {
var a struct {
Status string `json:"status"`
}
if err := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
rules := s.store.List()
@@ -74,12 +45,12 @@ func (s *Server) buildTools() []tool {
{
Name: "get_promotion",
Description: "Fetch a single promotion rule by id.",
InputSchema: object(props{"id": str("the promotion id")}, []string{"id"}),
invoke: func(args json.RawMessage) (any, error) {
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 := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
r, ok := s.store.Get(a.ID)
@@ -95,14 +66,14 @@ func (s *Server) buildTools() []tool {
"`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: object(props{
"promotion": obj("the full promotion rule object"),
InputSchema: pmcp.Object(pmcp.Props{
"promotion": pmcp.ObjectValue("the full promotion rule object"),
}, []string{"promotion"}),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(_ context.Context, args json.RawMessage) (any, error) {
var a struct {
Promotion json.RawMessage `json:"promotion"`
}
if err := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
var rule promotions.PromotionRule
@@ -125,16 +96,16 @@ func (s *Server) buildTools() []tool {
{
Name: "set_promotion_status",
Description: "Set a promotion's status (active|inactive|scheduled|expired) to toggle it on or off.",
InputSchema: object(props{
"id": str("the promotion id"),
"status": str("new status: active|inactive|scheduled|expired"),
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(args json.RawMessage) (any, error) {
Invoke: func(_ context.Context, args json.RawMessage) (any, error) {
var a struct {
ID string `json:"id"`
Status string `json:"status"`
}
if err := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
if !validStatus(a.Status) {
@@ -153,12 +124,12 @@ func (s *Server) buildTools() []tool {
{
Name: "delete_promotion",
Description: "Delete a promotion rule by id.",
InputSchema: object(props{"id": str("the promotion id")}, []string{"id"}),
invoke: func(args json.RawMessage) (any, error) {
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 := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
found, err := s.store.Delete(a.ID)
@@ -176,18 +147,18 @@ func (s *Server) buildTools() []tool {
Description: "Evaluate the current active promotions against a hypothetical cart total and report the " +
"matched discount. cartTotalIncVat is in öre incl. VAT (e.g. 5 000 000 = 50 000 kr). Useful for " +
"verifying Volymrabatt tiers without touching a real cart.",
InputSchema: object(props{
"cartTotalIncVat": integer("cart total incl. VAT, in öre"),
"itemQuantity": integer("optional total item quantity"),
"customerSegment": str("optional customer segment"),
InputSchema: pmcp.Object(pmcp.Props{
"cartTotalIncVat": pmcp.Integer("cart total incl. VAT, in öre"),
"itemQuantity": pmcp.Integer("optional total item quantity"),
"customerSegment": pmcp.String("optional customer segment"),
}, []string{"cartTotalIncVat"}),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(_ context.Context, args json.RawMessage) (any, error) {
var a struct {
CartTotalIncVat int64 `json:"cartTotalIncVat"`
ItemQuantity int `json:"itemQuantity"`
CustomerSegment string `json:"customerSegment"`
}
if err := decode(args, &a); err != nil {
if err := pmcp.Decode(args, &a); err != nil {
return nil, err
}
g := syntheticCart(a.CartTotalIncVat, a.ItemQuantity, s.defaultVatRate())
@@ -210,19 +181,19 @@ func (s *Server) buildTools() []tool {
}
}
// syntheticCart builds a one-line cart whose gross total is incVat ore so the
// promotion engine can be evaluated against an arbitrary total. defaultVatRate
// is the VAT rate as a float32 percentage (e.g. 25 = 25 %).
func syntheticCart(incVat int64, qty int, defaultVatRate float32) *cart.CartGrain {
// 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 defaultVatRate == 0 {
defaultVatRate = 25
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, defaultVatRate)},
{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.
@@ -240,55 +211,13 @@ func validStatus(s string) bool {
return false
}
// ---- result + schema helpers (mirrors the graph-cms MCP edge) -------------
func toolText(v any) map[string]any {
b, err := json.Marshal(v)
if err != nil {
return toolError(err)
}
return map[string]any{
"content": []map[string]any{{"type": "text", "text": string(b)}},
}
}
func toolError(err error) map[string]any {
return map[string]any{
"isError": true,
"content": []map[string]any{{"type": "text", "text": err.Error()}},
}
}
// decode unmarshals tool arguments, tolerating empty/absent arguments.
func decode(args json.RawMessage, v any) error {
if len(args) == 0 || string(args) == "null" {
return nil
}
if err := json.Unmarshal(args, v); err != nil {
return fmt.Errorf("invalid arguments: %w", err)
}
return nil
}
type props map[string]json.RawMessage
func object(p props, required []string) json.RawMessage {
m := map[string]any{
"type": "object",
"properties": p,
}
if len(required) > 0 {
m["required"] = required
}
b, _ := json.Marshal(m)
return b
}
func str(desc string) json.RawMessage { return scalar("string", desc) }
func obj(desc string) json.RawMessage { return scalar("object", desc) }
func integer(d string) json.RawMessage { return scalar("integer", d) }
func scalar(typ, desc string) json.RawMessage {
b, _ := json.Marshal(map[string]any{"type": typ, "description": desc})
return b
// 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("")
}