StillDeep SDK — Go client

Go client: protoc-gen-go-grpc, code generation, shift, first sale.

Prerequisites

  • Go 1.23+
  • protoc 25+
  • Plugins: protoc-gen-go, protoc-gen-go-grpc
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Setup

The .proto files declare no go_package: map each file to your Go package with M options.

mkdir my-pos-client && cd my-pos-client
go mod init example.com/my-pos-client
go get google.golang.org/grpc google.golang.org/protobuf

cp -r ../waxpos/src/public/StillDeep.Intelligence.Contracts/protos ./protos
mkdir -p generated

PKG=example.com/my-pos-client/generated
FILES="common receipt sales catalog member device events user shift"
OPTS=""
for f in $FILES; do OPTS="$OPTS --go_opt=M$f.proto=$PKG --go-grpc_opt=M$f.proto=$PKG"; done

protoc \
  --proto_path=./protos \
  --go_out=./generated \
  --go_opt=paths=source_relative \
  --go-grpc_out=./generated \
  --go-grpc_opt=paths=source_relative \
  $OPTS \
  $(for f in $FILES; do echo ./protos/$f.proto; done)

Example: sign in, open the shift, sell one item

Sequence and required fields: see getting-started.md.

package main

import (
    "context"
    "crypto/rand"
    "fmt"
    "log"
    "os"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/status"

    pb "example.com/my-pos-client/generated"
)

func key() string {
    b := make([]byte, 16)
    rand.Read(b)
    b[6], b[8] = b[6]&0x0f|0x40, b[8]&0x3f|0x80
    return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}

func main() {
    postID := os.Getenv("STILLDEEP_POST_ID") // required: id of this post
    sku := os.Getenv("STILLDEEP_SKU")        // product to sell
    method := "cash"                          // a code from ConfigService.GetPostConfig

    conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil { log.Fatal(err) }
    defer conn.Close()

    users := pb.NewUserServiceClient(conn)
    shifts := pb.NewShiftServiceClient(conn)
    sales := pb.NewSalesServiceClient(conn)
    ctx := context.Background()

    // 1. Authenticate (required).
    auth, err := users.Authenticate(ctx, &pb.AuthenticateRequest{
        PostId: postID, Mode: pb.AuthMode_AUTH_MODE_PASSWORD,
        Login: os.Getenv("STILLDEEP_LOGIN"), Password: os.Getenv("STILLDEEP_PASSWORD"),
    })
    if err != nil { log.Fatal(err) }

    // 2-3. Reuse the open shift, or open one.
    shift, err := shifts.GetCurrentShift(ctx, &pb.GetCurrentShiftRequest{PostId: postID})
    if status.Code(err) == codes.NotFound {
        opened, openErr := shifts.OpenShift(ctx, &pb.OpenShiftRequest{
            PostId: postID, UserId: auth.GetUser().GetUserId(),
            OpeningCash: &pb.Money{Cents: 0}, IdempotencyKey: key(),
        })
        if openErr != nil { log.Fatal(openErr) }
        shift, err = opened.GetShift(), nil
    }
    if err != nil { log.Fatal(err) }

    // Every following call carries x-shift-id.
    sctx := metadata.AppendToOutgoingContext(ctx, "x-shift-id", shift.GetShiftId())

    // 4. CreateCart (required).
    created, err := sales.CreateCart(sctx, &pb.CreateCartRequest{PostId: postID, IdempotencyKey: key()})
    if err != nil { log.Fatal(err) }
    cartID := created.GetCart().GetCartId()

    // 5. AddItem (required): product_id, sku or barcode; quantity in milli-units.
    if _, err = sales.AddItem(sctx, &pb.AddItemRequest{
        CartId: cartID, Sku: sku, Quantity: &pb.Quantity{MilliUnits: 1000}, IdempotencyKey: key(),
    }); err != nil { log.Fatal(err) }

    // 6. BeginPayment (optional): handle an extension question before paying.
    checkout, err := sales.BeginPayment(sctx, &pb.BeginPaymentRequest{CartId: cartID})
    if err != nil { log.Fatal(err) }
    if q := checkout.GetQuestion(); q != nil { log.Fatalf("answer required: %s", q.GetCode()) }

    // 8. ProcessPayment (required): pay the remaining amount.
    paid, err := sales.ProcessPayment(sctx, &pb.ProcessPaymentRequest{
        CartId: cartID, MethodCode: method, Amount: checkout.GetCart().GetRemainingDue(), IdempotencyKey: key(),
    })
    if err != nil { log.Fatal(err) }
    switch paid.GetPayment().GetStatus() {
    case pb.PaymentStatus_PAYMENT_STATUS_DECLINED, pb.PaymentStatus_PAYMENT_STATUS_FAILED:
        log.Fatal(paid.GetRefusalGuide().GetUserFacingMessage())
    }

    // 9. FinalizeTransaction (required).
    done, err := sales.FinalizeTransaction(sctx, &pb.FinalizeRequest{CartId: cartID, IdempotencyKey: key()})
    if err != nil { log.Fatal(err) }
    log.Printf("receipt %s, change %d cents", done.GetTransaction().GetReceiptNumber(), paid.GetChangeDue().GetCents())
}