ordersystem

package module
v0.0.0-...-3d6243c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2025 License: GPL-3.0 Imports: 15 Imported by: 0

README

ordersystem

ordersystem uses an SQLite Database, continuous replication using Litestream is recommended.

Please note

  • Check your BTCPay Server regularly for invoices which have been paid partially or late

BTCPay Server Configuration

  • User API Keys: enable btcpay.store.canviewinvoices and btcpay.store.cancreateinvoice
  • Store Webhook
    • Payload URL: https://example.com/rpc
    • Automatic redelivery: yes
    • Is enabled: yes
    • Events: "A new payment has been received", "An invoice has been settled"
  • Store access token
    • PublicKey: use the hex SIN which ordersystem writes to the log on startup

ToDo

  • client should see task ID
  • reshipping: integrate shipment number (if desired) into user interface

Documentation

Index

Constants

This section is empty.

Variables

View Source
var CollFSM = &FSM{
	Transition{State(Accepted), Bot, "confirm-payment", State(Active)},
	Transition{State(Accepted), Bot, "delete", State(Deleted)},
	Transition{State(Accepted), Client, "cancel", State(Cancelled)},
	Transition{State(Accepted), Client, "pay", State(Accepted)},
	Transition{State(Accepted), Store, "confirm-payment", State(Active)},
	Transition{State(Accepted), Store, "delete", State(Deleted)},
	Transition{State(Accepted), Store, "edit", State(Accepted)},
	Transition{State(Accepted), Store, "activate", State(Active)},
	Transition{State(Accepted), Store, "return", State(NeedsRevise)},
	Transition{State(Draft), Bot, "delete", State(Deleted)},
	Transition{State(Draft), Client, "delete", State(Deleted)},
	Transition{State(Draft), Client, "edit", State(Draft)},
	Transition{State(Draft), Client, "submit", State(Submitted)},
	Transition{State(Draft), Store, "submit", State(Submitted)},
	Transition{State(Submitted), Client, "message", State(Submitted)},
	Transition{State(Submitted), Store, "message", State(Submitted)},
	Transition{State(Finalized), Store, "message", State(Finalized)},
	Transition{State(Finalized), Bot, "archive", State(Archived)},
	Transition{State(NeedsRevise), Client, "cancel", State(Cancelled)},
	Transition{State(NeedsRevise), Client, "edit", State(NeedsRevise)},
	Transition{State(NeedsRevise), Client, "submit", State(Submitted)},
	Transition{State(Active), Bot, "confirm-payment", State(Active)},
	Transition{State(Active), Bot, "finalize", State(Finalized)},
	Transition{State(Active), Store, "confirm-payment", State(Accepted)},
	Transition{State(Active), Store, "confirm-payment", State(Active)},
	Transition{State(Active), Store, "confirm-pickup", State(Active)},
	Transition{State(Active), Store, "confirm-reshipped", State(Active)},
	Transition{State(Active), Store, "edit", State(Active)},
	Transition{State(Active), Store, "message", State(Active)},
	Transition{State(Spam), Bot, "delete", State(Deleted)},
	Transition{State(Submitted), Client, "cancel", State(Cancelled)},
	Transition{State(Submitted), Store, "accept", State(Accepted)},
	Transition{State(Submitted), Store, "edit", State(Submitted)},
	Transition{State(Submitted), Store, "mark-spam", State(Spam)},
	Transition{State(Submitted), Store, "reject", State(Rejected)},
	Transition{State(Submitted), Store, "return", State(NeedsRevise)},
	Transition{State(Active), Bot, "confirm-payment", State(Active)},
	Transition{State(Active), Client, "message", State(Active)},
	Transition{State(Active), Client, "pay", State(Active)},
	Transition{State(Active), Store, "confirm-payment", State(Accepted)},
	Transition{State(Active), Store, "confirm-payment", State(Active)},
	Transition{State(Active), Store, "edit", State(Active)},
	Transition{State(Active), Store, "message", State(Active)},
	Transition{State(Active), Client, "message", State(Active)},
	Transition{State(Accepted), Store, "message", State(Accepted)},
	Transition{State(Accepted), Client, "message", State(Accepted)},
	Transition{State(Finalized), Store, "activate", State(Active)},
}
View Source
var ErrNotFound = errors.New("not found")
View Source
var TaskFSM = &FSM{
	Transition{State(NotOrderedYet), Store, "confirm-ordered", State(Ordered)},
	Transition{State(NotOrderedYet), Store, "mark-failed", State(Failed)},
	Transition{State(Ordered), Store, "confirm-arrived", State(Ready)},
	Transition{State(Ordered), Store, "mark-failed", State(Failed)},
	Transition{State(Ready), Bot, "pickup-expired", State(Unfetched)},
	Transition{State(Ready), Store, "confirm-pickup", State(Fetched)},
	Transition{State(Ready), Store, "confirm-reshipped", State(Reshipped)},
}

Functions

func HashPassword

func HashPassword(password string) ([]byte, error)

Types

type Actor

type Actor string
const (
	Bot    Actor = "bot"
	Client Actor = "client"
	Store  Actor = "store"
)

func (Actor) IsClient

func (a Actor) IsClient() bool

for templates, so they don't have to use string literals

func (Actor) IsStore

func (a Actor) IsStore() bool

for templates, so they don't have to use string literals

func (Actor) Name

func (a Actor) Name() string

type AddCost

type AddCost struct {
	Name  string `json:"name"`
	Price int    `json:"price"` // positive (expenses) or negative (discount)
}

type Article

type Article struct {
	Link       string `json:"link"`
	Price      int    `json:"price"` // item price
	Properties string `json:"properties"`
	Quantity   int    `json:"quantity"`
}

func (Article) Sum

func (article Article) Sum() int

type ClientInput

type ClientInput struct {
	ClientContact             string `json:"client-contact"`
	ClientContactProtocol     string `json:"client-contact-protocol"`
	DeliveryMethod            string `json:"delivery-method"`
	ShippingAddressSupplement string `json:"shipping-address-supplement"`
	ShippingFirstName         string `json:"shipping-first-name"`
	ShippingLastName          string `json:"shipping-last-name"`
	ShippingPostcode          string `json:"shipping-postcode"`
	ShippingServiceID         string `json:"shipping-service"`
	ShippingStreet            string `json:"shipping-street"`
	ShippingStreetNumber      string `json:"shipping-street-number"`
	ShippingTown              string `json:"shipping-town"`
}

func (*ClientInput) ContactProtocols

func (data *ClientInput) ContactProtocols() []ContactProtocol

ClientContactProtocols returns available contact protocols. If an unknown (i.e. deprecated) protocol is stored in the database, it is returned as well.

func (*ClientInput) ShippingServices

func (data *ClientInput) ShippingServices() []ShippingService

ShippingServices returns available shipping services. If an unknown (i.e. deprecated) service is stored in the database, it is returned as well.

type CollState

type CollState string
const (
	Accepted    CollState = "accepted"
	Active      CollState = "active"
	Archived    CollState = "archived"
	Cancelled   CollState = "cancelled"
	Deleted     CollState = "deleted"
	Draft       CollState = "draft"
	Finalized   CollState = "finalized"
	NeedsRevise CollState = "needs-revise"
	Rejected    CollState = "rejected"
	Spam        CollState = "spam"
	Submitted   CollState = "submitted"
)

func (CollState) Description

func (s CollState) Description() template.HTML

func (CollState) Name

func (s CollState) Name() string

type Collection

type Collection struct {
	// stored as SQL row, but partly unmarshaled from user input, hence the JSON tags
	ID    string    `json:"-"`
	Pass  string    `json:"-"`
	State CollState `json:"-"`
	CollectionData
	Log   []Event  `json:"-"`
	Tasks TaskList `json:"tasks"`
}

func (*Collection) AuthorizedCollID

func (coll *Collection) AuthorizedCollID() string

AuthorizedCollID returns the collection ID. This is useful in templates, where we can pass a Collection or an "AuthorizedCollID" struct field.

func (*Collection) Balance

func (coll *Collection) Balance() int

func (*Collection) BotCan

func (coll *Collection) BotCan(action string) bool

func (*Collection) ClientCan

func (coll *Collection) ClientCan(action string) bool

func (*Collection) CompareHash

func (coll *Collection) CompareHash(password string) bool

func (*Collection) Due

func (coll *Collection) Due() int

func (*Collection) GetTask

func (coll *Collection) GetTask(id string) (*Task, bool)

func (*Collection) LatestEventSince

func (coll *Collection) LatestEventSince() (time.Duration, error)
func (coll *Collection) Link() string

Link returns an absolute URL without host, like "/collection/ABCDEFGHKL"

func (*Collection) MaxDate

func (coll *Collection) MaxDate() string

func (*Collection) Merge

func (coll *Collection) Merge(actor Actor, untrustedColl *Collection) error

Merge merges an untrusted collection into the receiving collection. coll.ID is never modified.

func (*Collection) MergeJSON

func (coll *Collection) MergeJSON(actor Actor, untrustedColl string) error

MergeJSON unmarshals JSON data and calls Merge.

func (*Collection) NumTasks

func (coll *Collection) NumTasks() int

func (*Collection) NumTasksAt

func (coll *Collection) NumTasksAt(state TaskState) int

func (*Collection) Paid

func (coll *Collection) Paid() int

func (*Collection) Reshipping

func (coll *Collection) Reshipping() int

func (*Collection) StoreCan

func (coll *Collection) StoreCan(action string) bool

func (*Collection) StoreCanTask

func (coll *Collection) StoreCanTask(action string, task *Task) bool

func (*Collection) Sum

func (coll *Collection) Sum() int

type CollectionData

type CollectionData struct {
	ClientInput
	StoreInput
	BookedInvoices         []string `json:"booked-invoices"`           // bitpay.Invoice.ID, booking is triggered by the invoice settled webhook
	ReceivedInTimePayments []string `json:"received-in-time-payments"` // bitpay.Invoice.InvoiceData.CryptoInfo.Payments.ID, event log like "Vorläufiger Zahlungseingang"
	ReceivedLatePayments   []string `json:"received-late-payments"`    // bitpay.Invoice.InvoiceData.CryptoInfo.Payments.ID, event log like "Verspäterer vorläufiger Zahlungseingang"
}

CollectionData is a separate struct so we can marshal it easily and store it in the SQL database.

func (*CollectionData) InvoiceHasBeenBooked

func (data *CollectionData) InvoiceHasBeenBooked(invoiceID string) bool

func (*CollectionData) PaymentHasBeenReceived

func (data *CollectionData) PaymentHasBeenReceived(paymentID string) bool

func (*CollectionData) PaymentHasBeenReceivedInTime

func (data *CollectionData) PaymentHasBeenReceivedInTime(paymentID string) bool

func (*CollectionData) PaymentHasBeenReceivedLate

func (data *CollectionData) PaymentHasBeenReceivedLate(paymentID string) bool

type ContactProtocol

type ContactProtocol struct {
	ID   string
	Name string
}

type DB

type DB struct {
	// contains filtered or unexported fields
}

func NewDB

func NewDB(sqlDB *sql.DB) (*DB, error)

func (*DB) Bot

func (db *DB) Bot(coll *Collection) error

Bot runs some automatic transitions. In order to avoid loops, it must be triggered by the ui only.

func (*DB) BotArchive

func (db *DB) BotArchive(coll *Collection) error

func (*DB) BotDelete

func (db *DB) BotDelete(coll *Collection) error

func (*DB) BotFinalize

func (db *DB) BotFinalize(coll *Collection) error

func (*DB) CreateCollection

func (db *DB) CreateCollection(coll *Collection) error

func (*DB) CreateEvent

func (db *DB) CreateEvent(actor Actor, coll *Collection, paid int, message string) error

CreateEvent creates an event. UpdateCollState should be preferred if the collection state changes.

func (*DB) Delete

func (db *DB) Delete(actor Actor, coll *Collection) error

func (*DB) ReadColl

func (db *DB) ReadColl(id string) (*Collection, error)

func (*DB) ReadCollPass

func (db *DB) ReadCollPass(id, pass string) (*Collection, error)

func (*DB) ReadColls

func (db *DB) ReadColls(state CollState) ([]string, error)

task count is currently filtered with NotOrderedYet

func (*DB) ReadState

func (db *DB) ReadState(id string) (CollState, error)

func (*DB) UpdateCollAndTasks

func (db *DB) UpdateCollAndTasks(coll *Collection) error

updates the collection given by coll.ID

func (*DB) UpdateCollState

func (db *DB) UpdateCollState(actor Actor, coll *Collection, newState CollState, paidAmount int, message string) error

coll must contain the old state

func (*DB) UpdateTaskState

func (db *DB) UpdateTaskState(actor Actor, task *Task, newState TaskState) error

task must contain the old state

type Date

type Date string // dateFmt

func Today

func Today() Date

func (Date) Format

func (d Date) Format() (string, error)

func (Date) Parse

func (d Date) Parse() (time.Time, error)

type Event

type Event struct {
	NewState CollState
	Date     Date
	Paid     int    // euro cents, adds to old values, positive amounts were paid by the client, negative amounts were paid by the store
	Text     string // CommonMark markdown
}

func (*Event) TextHTML

func (e *Event) TextHTML() template.HTML

type FSM

type FSM []Transition // easier than a map for a small number of transactions

func (*FSM) Can

func (fsm *FSM) Can(me Actor, from State, to State) bool

func (*FSM) CanAction

func (fsm *FSM) CanAction(me Actor, from State, action string) bool

func (*FSM) From

func (fsm *FSM) From(me Actor) []State

type ShippingService

type ShippingService struct {
	ID      string
	Name    string
	MinCost int
}

type State

type State string

type StoreInput

type StoreInput struct {
	ReshippingFee int `json:"reshipping-fee"`
}

type Task

type Task struct {
	// stored as SQL row, but partly unmarshaled from user input, hence the JSON tags
	ID    string    `json:"id"`
	State TaskState `json:"-"`
	TaskData
}

func (*Task) Fee

func (task *Task) Fee() int

func (*Task) Sum

func (task *Task) Sum() int

Sum is the sum of articles, shipping fee and additional costs. No store fee included.

func (*Task) TotalSum

func (task *Task) TotalSum() int

func (*Task) Writeable

func (task *Task) Writeable(actor Actor) bool

type TaskData

type TaskData struct {
	AddCosts    []AddCost `json:"add-costs"`
	Articles    []Article `json:"articles"`
	Merchant    string    `json:"merchant"`
	ShippingFee int       `json:"shipping-fee"`
}

TaskData is a separate struct so we can marshal it easily and store it in the SQL database.

type TaskList

type TaskList []*Task

func (*TaskList) Clear

func (tl *TaskList) Clear(actor Actor)

func (*TaskList) Insert

func (tl *TaskList) Insert(task *Task)

type TaskState

type TaskState string
const (
	Failed        TaskState = "failed"
	Fetched       TaskState = "fetched"
	NotOrderedYet TaskState = "not-ordered-yet" // initial state
	Ordered       TaskState = "ordered"
	Ready         TaskState = "ready"
	Reshipped     TaskState = "reshipped"
	Unfetched     TaskState = "unfetched"
)

func (TaskState) Description

func (s TaskState) Description() template.HTML

func (TaskState) Name

func (s TaskState) Name() string

type Transition

type Transition struct {
	// contains filtered or unexported fields
}

Directories

Path Synopsis
cmd
ordersystem command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL