diff --git a/deployment/order-manager.yaml b/deployment/order-manager.yaml new file mode 100644 index 0000000..5c1d986 --- /dev/null +++ b/deployment/order-manager.yaml @@ -0,0 +1,126 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: order-manager + arch: arm64 + name: order-manager-arm64 +spec: + replicas: 0 + selector: + matchLabels: + app: order-manager + arch: arm64 + template: + metadata: + labels: + app: order-manager + arch: arm64 + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: NotIn + values: + - masterpi + - key: kubernetes.io/arch + operator: In + values: + - arm64 + volumes: + - name: data + nfs: + path: /i-data/7a8af061/nfs/order-manager + server: 10.10.1.10 + imagePullSecrets: + - name: regcred + serviceAccountName: default + containers: + - image: registry.knatofs.se/go-order-manager:latest + name: order-manager-arm64 + imagePullPolicy: Always + lifecycle: + preStop: + exec: + command: ["sleep", "15"] + ports: + - containerPort: 8080 + name: web + volumeMounts: + - mountPath: "/data" + name: data + resources: + limits: + memory: "768Mi" + requests: + memory: "70Mi" + cpu: "1200m" + env: + - name: TZ + value: "Europe/Stockholm" + - name: KLARNA_API_USERNAME + valueFrom: + secretKeyRef: + name: klarna-api-credentials + key: username + - name: KLARNA_API_PASSWORD + valueFrom: + secretKeyRef: + name: klarna-api-credentials + key: password + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: AMQP_URL + value: "amqp://admin:12bananer@rabbitmq:5672/" + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name +--- +kind: Service +apiVersion: v1 +metadata: + name: order-manager + annotations: + prometheus.io/port: "8080" +spec: + selector: + app: order-manager + ports: + - name: web + port: 8080 +#--- +#apiVersion: networking.k8s.io/v1 +#kind: Ingress +#metadata: +# name: order-manager-ingress +# annotations: +# cert-manager.io/cluster-issuer: letsencrypt-prod +# # nginx.ingress.kubernetes.io/affinity: "cookie" +# # nginx.ingress.kubernetes.io/session-cookie-name: "cart-affinity" +# # nginx.ingress.kubernetes.io/session-cookie-expires: "172800" +# # nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" +# nginx.ingress.kubernetes.io/proxy-body-size: 4m +#spec: +# ingressClassName: nginx +# tls: +# - hosts: +# - orders.tornberg.me +# secretName: order-manager-tls-secret +# rules: +# - host: cart.tornberg.me +# http: +# paths: +# - path: / +# pathType: Prefix +# backend: +# service: +# name: order-manager +# port: +# number: 8080 \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..29626b1 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.tornberg.me/go-order-manager + +go 1.24.0 + +require github.com/rabbitmq/amqp091-go v1.10.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..024eebe --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= diff --git a/order.go b/order.go new file mode 100644 index 0000000..3b21589 --- /dev/null +++ b/order.go @@ -0,0 +1,169 @@ +package main + +type ( + LineType string + + // CheckoutOrder type is the request structure to create a new order from the Checkout API + Order struct { + ID string `json:"order_id,omitempty"` + PurchaseCountry string `json:"purchase_country"` + PurchaseCurrency string `json:"purchase_currency"` + Locale string `json:"locale"` + Status string `json:"status,omitempty"` + BillingAddress *Address `json:"billing_address,omitempty"` + ShippingAddress *Address `json:"shipping_address,omitempty"` + OrderAmount int `json:"order_amount"` + OrderTaxAmount int `json:"order_tax_amount"` + OrderLines []*Line `json:"order_lines"` + Customer *CheckoutCustomer `json:"customer,omitempty"` + MerchantURLS *CheckoutMerchantURLS `json:"merchant_urls"` + HTMLSnippet string `json:"html_snippet,omitempty"` + MerchantReference1 string `json:"merchant_reference1,omitempty"` + MerchantReference2 string `json:"merchant_reference2,omitempty"` + StartedAt string `json:"started_at,omitempty"` + CompletedAt string `json:"completed_at,omitempty"` + LastModifiedAt string `json:"last_modified_at,omitempty"` + Options *CheckoutOptions `json:"options,omitempty"` + Attachment *Attachment `json:"attachment,omitempty"` + ExternalPaymentMethods []*PaymentProvider `json:"external_payment_methods,omitempty"` + ExternalCheckouts []*PaymentProvider `json:"external_checkouts,omitempty"` + ShippingCountries []string `json:"shipping_countries,omitempty"` + ShippingOptions []*ShippingOption `json:"shipping_options,omitempty"` + MerchantData string `json:"merchant_data,omitempty"` + GUI *GUI `json:"gui,omitempty"` + MerchantRequested *AdditionalCheckBox `json:"merchant_requested,omitempty"` + SelectedShippingOption *ShippingOption `json:"selected_shipping_option,omitempty"` + ErrorCode string `json:"error_code,omitempty"` + ErrorMessages []string `json:"error_messages,omitempty"` + } + + // GUI type wraps the GUI options + GUI struct { + Options []string `json:"options,omitempty"` + } + + // ShippingOption type is part of the CheckoutOrder structure, represent the shipping options field + ShippingOption struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Promo string `json:"promo,omitempty"` + Price int `json:"price"` + TaxAmount int `json:"tax_amount"` + TaxRate int `json:"tax_rate"` + Preselected bool `json:"preselected,omitempty"` + ShippingMethod string `json:"shipping_method,omitempty"` + } + + // PaymentProvider type is part of the CheckoutOrder structure, represent the ExternalPaymentMethods and + // ExternalCheckouts field + PaymentProvider struct { + Name string `json:"name"` + RedirectURL string `json:"redirect_url"` + ImageURL string `json:"image_url,omitempty"` + Fee int `json:"fee,omitempty"` + Description string `json:"description,omitempty"` + Countries []string `json:"countries,omitempty"` + } + + Attachment struct { + ContentType string `json:"content_type"` + Body string `json:"body"` + } + + CheckoutOptions struct { + AcquiringChannel string `json:"acquiring_channel,omitempty"` + AllowSeparateShippingAddress bool `json:"allow_separate_shipping_address,omitempty"` + ColorButton string `json:"color_button,omitempty"` + ColorButtonText string `json:"color_button_text,omitempty"` + ColorCheckbox string `json:"color_checkbox,omitempty"` + ColorCheckboxCheckmark string `json:"color_checkbox_checkmark,omitempty"` + ColorHeader string `json:"color_header,omitempty"` + ColorLink string `json:"color_link,omitempty"` + DateOfBirthMandatory bool `json:"date_of_birth_mandatory,omitempty"` + ShippingDetails string `json:"shipping_details,omitempty"` + TitleMandatory bool `json:"title_mandatory,omitempty"` + AdditionalCheckbox *AdditionalCheckBox `json:"additional_checkbox"` + RadiusBorder string `json:"radius_border,omitempty"` + ShowSubtotalDetail bool `json:"show_subtotal_detail,omitempty"` + RequireValidateCallbackSuccess bool `json:"require_validate_callback_success,omitempty"` + AllowGlobalBillingCountries bool `json:"allow_global_billing_countries,omitempty"` + } + + AdditionalCheckBox struct { + Text string `json:"text"` + Checked bool `json:"checked"` + Required bool `json:"required"` + } + + CheckoutMerchantURLS struct { + // URL of merchant terms and conditions. Should be different than checkout, confirmation and push URLs. + // (max 2000 characters) + Terms string `json:"terms"` + + // URL of merchant checkout page. Should be different than terms, confirmation and push URLs. + // (max 2000 characters) + Checkout string `json:"checkout"` + + // URL of merchant confirmation page. Should be different than checkout and confirmation URLs. + // (max 2000 characters) + Confirmation string `json:"confirmation"` + + // URL that will be requested when an order is completed. Should be different than checkout and + // confirmation URLs. (max 2000 characters) + Push string `json:"push"` + // URL that will be requested for final merchant validation. (must be https, max 2000 characters) + Validation string `json:"validation,omitempty"` + + // URL for shipping option update. (must be https, max 2000 characters) + ShippingOptionUpdate string `json:"shipping_option_update,omitempty"` + + // URL for shipping, tax and purchase currency updates. Will be called on address changes. + // (must be https, max 2000 characters) + AddressUpdate string `json:"address_update,omitempty"` + + // URL for notifications on pending orders. (max 2000 characters) + Notification string `json:"notification,omitempty"` + + // URL for shipping, tax and purchase currency updates. Will be called on purchase country changes. + // (must be https, max 2000 characters) + CountryChange string `json:"country_change,omitempty"` + } + + CheckoutCustomer struct { + // DateOfBirth in string representation 2006-01-02 + DateOfBirth string `json:"date_of_birth"` + } + + // Address type define the address object (json serializable) being used for the API to represent billing & + // shipping addresses + Address struct { + GivenName string `json:"given_name,omitempty"` + FamilyName string `json:"family_name,omitempty"` + Email string `json:"email,omitempty"` + Title string `json:"title,omitempty"` + StreetAddress string `json:"street_address,omitempty"` + StreetAddress2 string `json:"street_address2,omitempty"` + PostalCode string `json:"postal_code,omitempty"` + City string `json:"city,omitempty"` + Region string `json:"region,omitempty"` + Phone string `json:"phone,omitempty"` + Country string `json:"country,omitempty"` + } + + Line struct { + Type string `json:"type,omitempty"` + Reference string `json:"reference,omitempty"` + Name string `json:"name"` + Quantity int `json:"quantity"` + QuantityUnit string `json:"quantity_unit,omitempty"` + UnitPrice int `json:"unit_price"` + TaxRate int `json:"tax_rate"` + TotalAmount int `json:"total_amount"` + TotalDiscountAmount int `json:"total_discount_amount,omitempty"` + TotalTaxAmount int `json:"total_tax_amount"` + MerchantData string `json:"merchant_data,omitempty"` + ProductURL string `json:"product_url,omitempty"` + ImageURL string `json:"image_url,omitempty"` + } +) diff --git a/order_client.go b/order_client.go new file mode 100644 index 0000000..c6749d7 --- /dev/null +++ b/order_client.go @@ -0,0 +1,98 @@ +package main + +import ( + "encoding/json" + "log" + + amqp "github.com/rabbitmq/amqp091-go" +) + +type UpdateHandler interface { + OrderPlaced(order Order) +} + +type RabbitTransportClient struct { + Url string + + OrderTopic string + ClientName string + handler UpdateHandler + connection *amqp.Connection + channel *amqp.Channel + quit chan bool +} + +func (t *RabbitTransportClient) declareBindAndConsume(topic string) (<-chan amqp.Delivery, error) { + q, err := t.channel.QueueDeclare( + "", // name + false, // durable + false, // delete when unused + true, // exclusive + false, // no-wait + nil, // arguments + ) + if err != nil { + return nil, err + } + err = t.channel.QueueBind(q.Name, topic, topic, false, nil) + if err != nil { + return nil, err + } + return t.channel.Consume( + q.Name, + "", + true, + false, + false, + false, + nil, + ) +} + +func (t *RabbitTransportClient) Connect(handler UpdateHandler) error { + conn, err := amqp.DialConfig(t.Url, amqp.Config{ + Properties: amqp.NewConnectionProperties(), + }) + //conn.Config.Vhost = t.VHost + t.quit = make(chan bool) + if err != nil { + return err + } + t.connection = conn + ch, err := conn.Channel() + if err != nil { + return err + } + t.handler = handler + t.channel = ch + toAdd, err := t.declareBindAndConsume(t.OrderTopic) + if err != nil { + return err + } + log.Printf("Connected to rabbit upsert topic: %s", t.OrderTopic) + go func(msgs <-chan amqp.Delivery) { + for d := range msgs { + + var order Order + if err := json.Unmarshal(d.Body, &order); err == nil { + log.Printf("Got order") + t.handler.OrderPlaced(order) + } else { + log.Printf("Failed to unmarshal upset message %v", err) + } + } + }(toAdd) + + return nil +} + +func (t *RabbitTransportClient) Close() { + if (t.channel != nil) && (!t.channel.IsClosed()) { + t.channel.Close() + } + if (t.connection != nil) && (!t.connection.IsClosed()) { + t.connection.Close() + } + //t.quit <- true + +}