feat(release): beelloo v0.1
This commit is contained in:
67
internal/invoice/money.go
Normal file
67
internal/invoice/money.go
Normal file
@ -0,0 +1,67 @@
|
||||
package invoice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseMoney(input string) (Money, error) {
|
||||
clean := strings.TrimSpace(input)
|
||||
if clean == "" {
|
||||
return 0, errors.New("empty money value")
|
||||
}
|
||||
sign := int64(1)
|
||||
if strings.HasPrefix(clean, "-") {
|
||||
sign = -1
|
||||
clean = strings.TrimSpace(strings.TrimPrefix(clean, "-"))
|
||||
}
|
||||
clean = strings.ReplaceAll(clean, " ", "")
|
||||
clean = strings.ReplaceAll(clean, ",", ".")
|
||||
if strings.HasPrefix(clean, ".") || strings.HasSuffix(clean, ".") {
|
||||
return 0, fmt.Errorf("invalid money value: %q", input)
|
||||
}
|
||||
parts := strings.Split(clean, ".")
|
||||
if len(parts) > 2 {
|
||||
return 0, fmt.Errorf("invalid money value: %q", input)
|
||||
}
|
||||
whole := parts[0]
|
||||
frac := ""
|
||||
if len(parts) == 2 {
|
||||
frac = parts[1]
|
||||
}
|
||||
if whole == "" {
|
||||
whole = "0"
|
||||
}
|
||||
for _, r := range whole {
|
||||
if r < '0' || r > '9' {
|
||||
return 0, fmt.Errorf("invalid money value: %q", input)
|
||||
}
|
||||
}
|
||||
for _, r := range frac {
|
||||
if r < '0' || r > '9' {
|
||||
return 0, fmt.Errorf("invalid money value: %q", input)
|
||||
}
|
||||
}
|
||||
if len(frac) > 2 {
|
||||
return 0, fmt.Errorf("invalid money value: %q", input)
|
||||
}
|
||||
for len(frac) < 2 {
|
||||
frac += "0"
|
||||
}
|
||||
var wholeValue int64
|
||||
for _, r := range whole {
|
||||
wholeValue = wholeValue*10 + int64(r-'0')
|
||||
}
|
||||
var fracValue int64
|
||||
for _, r := range frac {
|
||||
fracValue = fracValue*10 + int64(r-'0')
|
||||
}
|
||||
return Money(sign * (wholeValue*100 + fracValue)), nil
|
||||
}
|
||||
|
||||
func formatCents(cents int64) string {
|
||||
whole := cents / 100
|
||||
frac := cents % 100
|
||||
return fmt.Sprintf("%d.%02d", whole, frac)
|
||||
}
|
||||
26
internal/invoice/money_test.go
Normal file
26
internal/invoice/money_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package invoice
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseMoney(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
want Money
|
||||
}{
|
||||
{"1175", 117500},
|
||||
{"1175.5", 117550},
|
||||
{"1175,50", 117550},
|
||||
{"0.01", 1},
|
||||
{"-15", -1500},
|
||||
{"-0.5", -50},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got, err := ParseMoney(tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseMoney(%q) unexpected error: %v", tc.input, err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("ParseMoney(%q)=%v want %v", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
229
internal/invoice/parse.go
Normal file
229
internal/invoice/parse.go
Normal file
@ -0,0 +1,229 @@
|
||||
package invoice
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ParseMarkdown(r io.Reader) (Document, error) {
|
||||
var doc Document
|
||||
var section string
|
||||
var inAddress bool
|
||||
var tableHeader []string
|
||||
scanner := bufio.NewScanner(r)
|
||||
lineNo := 0
|
||||
for scanner.Scan() {
|
||||
lineNo++
|
||||
line := strings.TrimRight(scanner.Text(), "\r")
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "## ") {
|
||||
section = strings.TrimSpace(strings.TrimPrefix(trimmed, "## "))
|
||||
inAddress = false
|
||||
if strings.EqualFold(section, "Prestations") {
|
||||
tableHeader = nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
if trimmed == "" {
|
||||
inAddress = false
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.ToLower(section) {
|
||||
case "seller":
|
||||
if ok := parsePartyLine(&doc.Seller, line, &inAddress); ok {
|
||||
continue
|
||||
}
|
||||
case "buyer":
|
||||
if ok := parsePartyLine(&doc.Buyer, line, &inAddress); ok {
|
||||
continue
|
||||
}
|
||||
case "invoice":
|
||||
key, value, ok := splitKeyValue(line)
|
||||
if !ok {
|
||||
return doc, fmt.Errorf("line %d: expected key/value in Invoice section", lineNo)
|
||||
}
|
||||
switch normalizeKey(key) {
|
||||
case "number":
|
||||
doc.Invoice.Number = value
|
||||
case "subject":
|
||||
doc.Invoice.Subject = value
|
||||
case "location":
|
||||
doc.Invoice.Location = value
|
||||
case "date":
|
||||
parsed, err := time.Parse("2006-01-02", value)
|
||||
if err != nil {
|
||||
return doc, fmt.Errorf("line %d: invalid date %q", lineNo, value)
|
||||
}
|
||||
doc.Invoice.Date = parsed
|
||||
case "description":
|
||||
doc.Invoice.Description = value
|
||||
default:
|
||||
return doc, fmt.Errorf("line %d: unknown invoice field %q", lineNo, key)
|
||||
}
|
||||
case "items":
|
||||
if !strings.Contains(trimmed, "|") {
|
||||
return doc, fmt.Errorf("line %d: expected table row in Items section", lineNo)
|
||||
}
|
||||
cells := splitTableRow(line)
|
||||
if len(cells) == 0 {
|
||||
continue
|
||||
}
|
||||
if tableHeader == nil {
|
||||
tableHeader = cells
|
||||
continue
|
||||
}
|
||||
if isSeparatorRow(cells) {
|
||||
continue
|
||||
}
|
||||
item, err := parseItemRow(tableHeader, cells)
|
||||
if err != nil {
|
||||
return doc, fmt.Errorf("line %d: %w", lineNo, err)
|
||||
}
|
||||
doc.Items = append(doc.Items, item)
|
||||
case "payment":
|
||||
key, value, ok := splitKeyValue(line)
|
||||
if !ok {
|
||||
return doc, fmt.Errorf("line %d: expected key/value in Payment section", lineNo)
|
||||
}
|
||||
switch normalizeKey(key) {
|
||||
case "holder":
|
||||
doc.Payment.Holder = value
|
||||
case "iban":
|
||||
doc.Payment.IBAN = value
|
||||
default:
|
||||
return doc, fmt.Errorf("line %d: unknown payment field %q", lineNo, key)
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return doc, err
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func parsePartyLine(party *Party, line string, inAddress *bool) bool {
|
||||
if *inAddress && (strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t")) {
|
||||
party.Address = append(party.Address, strings.TrimSpace(line))
|
||||
return true
|
||||
}
|
||||
key, value, ok := splitKeyValue(line)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
switch normalizeKey(key) {
|
||||
case "name":
|
||||
party.Name = value
|
||||
*inAddress = false
|
||||
return true
|
||||
case "address":
|
||||
*inAddress = true
|
||||
if value != "" {
|
||||
party.Address = append(party.Address, value)
|
||||
}
|
||||
return true
|
||||
case "siret":
|
||||
party.SIRET = value
|
||||
*inAddress = false
|
||||
return true
|
||||
case "email":
|
||||
party.Email = value
|
||||
*inAddress = false
|
||||
return true
|
||||
case "phone":
|
||||
party.Phone = value
|
||||
*inAddress = false
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func splitKeyValue(line string) (string, string, bool) {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", "", false
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
return key, value, true
|
||||
}
|
||||
|
||||
func normalizeKey(key string) string {
|
||||
k := strings.ToLower(strings.TrimSpace(key))
|
||||
replacer := strings.NewReplacer("é", "e", "è", "e", "ê", "e", "à", "a", "ç", "c")
|
||||
return replacer.Replace(k)
|
||||
}
|
||||
|
||||
func splitTableRow(line string) []string {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "|") {
|
||||
return nil
|
||||
}
|
||||
trimmed = strings.TrimPrefix(trimmed, "|")
|
||||
trimmed = strings.TrimSuffix(trimmed, "|")
|
||||
parts := strings.Split(trimmed, "|")
|
||||
var cells []string
|
||||
for _, part := range parts {
|
||||
cells = append(cells, strings.TrimSpace(part))
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
||||
func isSeparatorRow(cells []string) bool {
|
||||
for _, cell := range cells {
|
||||
if cell == "" {
|
||||
continue
|
||||
}
|
||||
for _, r := range cell {
|
||||
if r != '-' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func parseItemRow(header, row []string) (Item, error) {
|
||||
if len(row) < len(header) {
|
||||
return Item{}, errors.New("row has fewer columns than header")
|
||||
}
|
||||
idxDesignation := -1
|
||||
idxPrice := -1
|
||||
idxQty := -1
|
||||
for i, col := range header {
|
||||
n := normalizeKey(col)
|
||||
switch {
|
||||
case strings.Contains(n, "designation"):
|
||||
idxDesignation = i
|
||||
case strings.Contains(n, "unitprice"), strings.Contains(n, "unit price"), strings.Contains(n, "price"), strings.Contains(n, "prix"):
|
||||
idxPrice = i
|
||||
case strings.Contains(n, "quantity"), strings.Contains(n, "quantite"), strings.Contains(n, "qty"):
|
||||
idxQty = i
|
||||
}
|
||||
}
|
||||
if idxDesignation == -1 || idxPrice == -1 || idxQty == -1 {
|
||||
return Item{}, errors.New("table header must include Designation, Unit price, Quantity")
|
||||
}
|
||||
item := Item{Designation: row[idxDesignation]}
|
||||
if item.Designation == "" {
|
||||
return Item{}, errors.New("empty designation")
|
||||
}
|
||||
price, err := ParseMoney(row[idxPrice])
|
||||
if err != nil {
|
||||
return Item{}, fmt.Errorf("invalid unit price: %w", err)
|
||||
}
|
||||
item.UnitPrice = price
|
||||
qty, err := ParseQuantity(row[idxQty])
|
||||
if err != nil {
|
||||
return Item{}, fmt.Errorf("invalid quantity: %w", err)
|
||||
}
|
||||
item.Quantity = qty
|
||||
return item, nil
|
||||
}
|
||||
48
internal/invoice/parse_test.go
Normal file
48
internal/invoice/parse_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package invoice
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseMarkdownSample(t *testing.T) {
|
||||
file, err := os.Open("../../testdata/sample.md")
|
||||
if err != nil {
|
||||
t.Fatalf("open sample: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
doc, err := ParseMarkdown(file)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseMarkdown: %v", err)
|
||||
}
|
||||
if doc.Seller.Name != "Alice Example" {
|
||||
t.Fatalf("seller name mismatch: %q", doc.Seller.Name)
|
||||
}
|
||||
if doc.Seller.Email != "alice@example.com" {
|
||||
t.Fatalf("seller email mismatch: %q", doc.Seller.Email)
|
||||
}
|
||||
if doc.Seller.Phone != "01 02 03 04 05" {
|
||||
t.Fatalf("seller phone mismatch: %q", doc.Seller.Phone)
|
||||
}
|
||||
if doc.Buyer.Name != "Example Corp" {
|
||||
t.Fatalf("buyer name mismatch: %q", doc.Buyer.Name)
|
||||
}
|
||||
if doc.Invoice.Number != "20250407" {
|
||||
t.Fatalf("invoice number mismatch: %q", doc.Invoice.Number)
|
||||
}
|
||||
if len(doc.Items) != 1 {
|
||||
t.Fatalf("expected 1 item, got %d", len(doc.Items))
|
||||
}
|
||||
if !strings.HasPrefix(doc.Items[0].Designation, "Forfait IT") {
|
||||
t.Fatalf("designation mismatch: %q", doc.Items[0].Designation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMissingFields(t *testing.T) {
|
||||
doc := Document{}
|
||||
if err := Validate(&doc); err == nil {
|
||||
t.Fatalf("expected validation error")
|
||||
}
|
||||
}
|
||||
70
internal/invoice/quantity.go
Normal file
70
internal/invoice/quantity.go
Normal file
@ -0,0 +1,70 @@
|
||||
package invoice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Quantity stores hundredths (e.g. 0.5 => 50).
|
||||
func ParseQuantity(input string) (Quantity, error) {
|
||||
clean := strings.TrimSpace(input)
|
||||
if clean == "" {
|
||||
return 0, errors.New("empty quantity")
|
||||
}
|
||||
clean = strings.ReplaceAll(clean, " ", "")
|
||||
clean = strings.ReplaceAll(clean, ",", ".")
|
||||
if strings.HasPrefix(clean, ".") || strings.HasSuffix(clean, ".") {
|
||||
return 0, fmt.Errorf("invalid quantity: %q", input)
|
||||
}
|
||||
parts := strings.Split(clean, ".")
|
||||
if len(parts) > 2 {
|
||||
return 0, fmt.Errorf("invalid quantity: %q", input)
|
||||
}
|
||||
whole := parts[0]
|
||||
frac := ""
|
||||
if len(parts) == 2 {
|
||||
frac = parts[1]
|
||||
}
|
||||
if whole == "" {
|
||||
whole = "0"
|
||||
}
|
||||
for _, r := range whole {
|
||||
if r < '0' || r > '9' {
|
||||
return 0, fmt.Errorf("invalid quantity: %q", input)
|
||||
}
|
||||
}
|
||||
for _, r := range frac {
|
||||
if r < '0' || r > '9' {
|
||||
return 0, fmt.Errorf("invalid quantity: %q", input)
|
||||
}
|
||||
}
|
||||
if len(frac) > 2 {
|
||||
return 0, fmt.Errorf("invalid quantity: %q", input)
|
||||
}
|
||||
for len(frac) < 2 {
|
||||
frac += "0"
|
||||
}
|
||||
var wholeValue int64
|
||||
for _, r := range whole {
|
||||
wholeValue = wholeValue*10 + int64(r-'0')
|
||||
}
|
||||
var fracValue int64
|
||||
for _, r := range frac {
|
||||
fracValue = fracValue*10 + int64(r-'0')
|
||||
}
|
||||
return Quantity(wholeValue*100 + fracValue), nil
|
||||
}
|
||||
|
||||
func FormatQuantity(q Quantity) string {
|
||||
value := int64(q)
|
||||
whole := value / 100
|
||||
frac := value % 100
|
||||
if frac == 0 {
|
||||
return fmt.Sprintf("%d", whole)
|
||||
}
|
||||
if frac%10 == 0 {
|
||||
return fmt.Sprintf("%d.%d", whole, frac/10)
|
||||
}
|
||||
return fmt.Sprintf("%d.%02d", whole, frac)
|
||||
}
|
||||
52
internal/invoice/quantity_test.go
Normal file
52
internal/invoice/quantity_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package invoice
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseQuantity(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
want Quantity
|
||||
}{
|
||||
{"1", 100},
|
||||
{"0.5", 50},
|
||||
{"2,25", 225},
|
||||
{"10.00", 1000},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got, err := ParseQuantity(tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseQuantity(%q) unexpected error: %v", tc.input, err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("ParseQuantity(%q)=%v want %v", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatQuantity(t *testing.T) {
|
||||
cases := []struct {
|
||||
input Quantity
|
||||
want string
|
||||
}{
|
||||
{100, "1"},
|
||||
{50, "0.5"},
|
||||
{225, "2.25"},
|
||||
{1010, "10.1"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := FormatQuantity(tc.input)
|
||||
if got != tc.want {
|
||||
t.Fatalf("FormatQuantity(%v)=%q want %q", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineAmountNegative(t *testing.T) {
|
||||
item := Item{
|
||||
UnitPrice: -29500,
|
||||
Quantity: 100,
|
||||
}
|
||||
if got := LineAmount(item); got != -29500 {
|
||||
t.Fatalf("LineAmount negative mismatch: %v", got)
|
||||
}
|
||||
}
|
||||
34
internal/invoice/scaffold.go
Normal file
34
internal/invoice/scaffold.go
Normal file
@ -0,0 +1,34 @@
|
||||
package invoice
|
||||
|
||||
const ScaffoldMarkdown = `# Invoice
|
||||
|
||||
## Seller
|
||||
Name:
|
||||
Address:
|
||||
|
||||
Email:
|
||||
Phone:
|
||||
SIRET:
|
||||
|
||||
## Buyer
|
||||
Name:
|
||||
Address:
|
||||
|
||||
SIRET:
|
||||
|
||||
## Invoice
|
||||
Number:
|
||||
Subject: Facture pour prestations de service
|
||||
Location:
|
||||
Date: 2025-04-07
|
||||
Description:
|
||||
|
||||
## Items
|
||||
| Designation | Unit price | Quantity |
|
||||
| --- | --- | --- |
|
||||
| | | |
|
||||
|
||||
## Payment
|
||||
Holder:
|
||||
IBAN:
|
||||
`
|
||||
57
internal/invoice/types.go
Normal file
57
internal/invoice/types.go
Normal file
@ -0,0 +1,57 @@
|
||||
package invoice
|
||||
|
||||
import "time"
|
||||
|
||||
type Document struct {
|
||||
Seller Party
|
||||
Buyer Party
|
||||
Invoice InvoiceInfo
|
||||
Items []Item
|
||||
Payment PaymentInfo
|
||||
Totals Totals
|
||||
}
|
||||
|
||||
type Party struct {
|
||||
Name string
|
||||
Address []string
|
||||
SIRET string
|
||||
Email string
|
||||
Phone string
|
||||
}
|
||||
|
||||
type InvoiceInfo struct {
|
||||
Number string
|
||||
Subject string
|
||||
Location string
|
||||
Date time.Time
|
||||
Description string
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Designation string
|
||||
UnitPrice Money
|
||||
Quantity Quantity
|
||||
}
|
||||
|
||||
type PaymentInfo struct {
|
||||
Holder string
|
||||
IBAN string
|
||||
}
|
||||
|
||||
type Totals struct {
|
||||
TotalExclTax Money
|
||||
}
|
||||
|
||||
type Money int64
|
||||
|
||||
type Quantity int64
|
||||
|
||||
func (m Money) String() string {
|
||||
sign := ""
|
||||
value := m
|
||||
if value < 0 {
|
||||
sign = "-"
|
||||
value = -value
|
||||
}
|
||||
return sign + formatCents(int64(value))
|
||||
}
|
||||
98
internal/invoice/validate.go
Normal file
98
internal/invoice/validate.go
Normal file
@ -0,0 +1,98 @@
|
||||
package invoice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func Validate(doc *Document) error {
|
||||
var problems []string
|
||||
if doc.Seller.Name == "" {
|
||||
problems = append(problems, "missing vendeur nom")
|
||||
}
|
||||
if len(doc.Seller.Address) == 0 {
|
||||
problems = append(problems, "missing vendeur adresse")
|
||||
}
|
||||
if doc.Seller.Email == "" {
|
||||
problems = append(problems, "missing vendeur email")
|
||||
}
|
||||
if doc.Seller.Phone == "" {
|
||||
problems = append(problems, "missing vendeur telephone")
|
||||
}
|
||||
if doc.Buyer.Name == "" {
|
||||
problems = append(problems, "missing acheteur nom")
|
||||
}
|
||||
if len(doc.Buyer.Address) == 0 {
|
||||
problems = append(problems, "missing acheteur adresse")
|
||||
}
|
||||
if doc.Invoice.Number == "" {
|
||||
problems = append(problems, "missing facture numero")
|
||||
}
|
||||
if doc.Invoice.Subject == "" {
|
||||
problems = append(problems, "missing facture objet")
|
||||
}
|
||||
if doc.Invoice.Location == "" {
|
||||
problems = append(problems, "missing facture lieu")
|
||||
}
|
||||
if doc.Invoice.Date.IsZero() {
|
||||
problems = append(problems, "missing facture date")
|
||||
}
|
||||
if doc.Invoice.Description == "" {
|
||||
problems = append(problems, "missing facture description")
|
||||
}
|
||||
if len(doc.Items) == 0 {
|
||||
problems = append(problems, "missing prestations")
|
||||
}
|
||||
if doc.Payment.Holder == "" {
|
||||
problems = append(problems, "missing paiement titulaire")
|
||||
}
|
||||
if doc.Payment.IBAN == "" {
|
||||
problems = append(problems, "missing paiement IBAN")
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
return errors.New(stringsJoin(problems, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ComputeTotals(doc *Document) {
|
||||
var total Money
|
||||
for _, item := range doc.Items {
|
||||
line := LineAmount(item)
|
||||
total += line
|
||||
}
|
||||
doc.Totals.TotalExclTax = total
|
||||
}
|
||||
|
||||
func stringsJoin(values []string, sep string) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
out := values[0]
|
||||
for i := 1; i < len(values); i++ {
|
||||
out += sep + values[i]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func FormatMoney(m Money) string {
|
||||
return m.String()
|
||||
}
|
||||
|
||||
func LineAmount(item Item) Money {
|
||||
// Quantity is stored in hundredths. Round to nearest cent.
|
||||
cents := int64(item.UnitPrice)
|
||||
qty := int64(item.Quantity)
|
||||
product := cents * qty
|
||||
if product >= 0 {
|
||||
return Money((product + 50) / 100)
|
||||
}
|
||||
return Money((product - 50) / 100)
|
||||
}
|
||||
|
||||
func RequireNoError(err error, msg string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", msg, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user