Extend 'order_placed' endpoint to handle different item categories
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Luca 2023-03-06 00:08:45 +01:00
parent 5cdddc21ed
commit 51211e641b
3 changed files with 137 additions and 53 deletions

24
internal/pretix/client.go Normal file
View File

@ -0,0 +1,24 @@
package pretix
import (
"fmt"
"net/http"
"strings"
)
func (s Server) buildURL(endpoint string, args ...any) string {
baseURL := strings.TrimSuffix(s.config.BaseURL, "/")
return fmt.Sprintf(endpoint, append([]any{baseURL}, args...)...)
}
func (s Server) doGetRequest(url string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprint("Token ", s.config.APIToken))
return s.client.Do(req)
}

View File

@ -3,6 +3,7 @@ package pretix
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/rand"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@ -15,8 +16,17 @@ const (
eventOrderPlaced = "pretix.event.order.placed" eventOrderPlaced = "pretix.event.order.placed"
urlIndividualOrder = "%s/api/v1/organizers/%s/events/%s/orders/%s/" urlIndividualOrder = "%s/api/v1/organizers/%s/events/%s/orders/%s/"
urlItem = "%s/api/v1/organizers/%s/events/%s/items/%d/"
urlItemCategory = "%s/api/v1/organizers/%s/events/%s/categories/%d/"
) )
var adverbs = []string{" Außerdem ", " Zusätzlich ", " Weiterhin ", " Des Weiteren "}
type categoryBin struct {
NumFree uint
NumPaid uint
}
func (s Server) orderPlaced(w http.ResponseWriter, r *http.Request) { func (s Server) orderPlaced(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-Type") != "application/json" { if r.Header.Get("Content-Type") != "application/json" {
writeStatus(w, http.StatusBadRequest) writeStatus(w, http.StatusBadRequest)
@ -42,16 +52,7 @@ func (s Server) orderPlaced(w http.ResponseWriter, r *http.Request) {
return return
} }
req, err := http.NewRequest(http.MethodGet, s.buildURL(urlIndividualOrder, data.Organizer, data.Event, data.Code), nil) resp, err := s.doGetRequest(s.buildURL(urlIndividualOrder, data.Organizer, data.Event, data.Code))
if err != nil {
writeStatus(w, http.StatusInternalServerError)
return
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprint("Token ", s.config.APIToken))
resp, err := s.client.Do(req)
if err != nil { if err != nil {
writeStatus(w, http.StatusInternalServerError) writeStatus(w, http.StatusInternalServerError)
return return
@ -73,6 +74,7 @@ func (s Server) orderPlaced(w http.ResponseWriter, r *http.Request) {
Code string Code string
Datetime time.Time Datetime time.Time
Positions []struct{ Positions []struct{
Item uint
Price string Price string
} }
Total string Total string
@ -97,59 +99,80 @@ func (s Server) orderPlaced(w http.ResponseWriter, r *http.Request) {
} }
if rowsAffected > 0 && order.Datetime.Add(72 * time.Hour).After(time.Now()) { // pretix retries failed webhooks for up to three days if rowsAffected > 0 && order.Datetime.Add(72 * time.Hour).After(time.Now()) { // pretix retries failed webhooks for up to three days
numFree := 0 categories := map[string]categoryBin{}
numPaid := 0
for _, position := range order.Positions { for _, position := range order.Positions {
euros, cents, err := parsePrice(position.Price) euros, cents, err := parsePrice(position.Price)
if err != nil { if err != nil {
continue continue
} }
name := s.fetchCategory(data.Organizer, data.Event, position.Item)
category := categories[name]
if euros > 0 || cents > 0 { if euros > 0 || cents > 0 {
numPaid++ category.NumPaid++
} else { } else {
numFree++ category.NumFree++
} }
categories[name] = category
} }
var verb, free, conjunction, paid, noun, total string var body strings.Builder
if numFree+numPaid == 1 { body.WriteString("\U0001f389")
verb = "wurde "
} else { rng := rand.New(rand.NewSource(time.Now().Unix()))
verb = "wurden "
} adverb := " Es "
if numFree == 1 { for category, bin := range categories {
free = "ein kostenloses " var verb, free, conjunction, paid, noun, preposition, quote, total string
} else if numFree > 1 { if bin.NumFree+bin.NumPaid == 1 {
free = fmt.Sprint(numFree, " kostenlose ") verb = "wurde "
} } else {
if numPaid == 1 { verb = "wurden "
paid = "ein bezahltes "
} else if numPaid > 1 {
paid = fmt.Sprint(numPaid, " bezahlte ")
}
if numFree > 0 && numPaid > 0 {
conjunction = "und "
}
if numPaid == 1 || numPaid == 0 && numFree == 1 { // match number of adjective immediately before noun
noun = "Ticket "
} else {
noun = "Tickets "
}
if totalEuros, totalCents, err := parsePrice(order.Total); err == nil && (totalEuros > 0 || totalCents > 0) {
var adverb, fraction string
if numPaid > 1 {
adverb = "insgesamt "
} }
if totalCents > 0 { if bin.NumFree == 1 {
fraction = fmt.Sprintf(",%02d", totalCents) free = "ein kostenloses "
} else if bin.NumFree > 1 {
free = fmt.Sprint(bin.NumFree, " kostenlose ")
} }
total = fmt.Sprint("für ", adverb, totalEuros, fraction, " Euro ") if bin.NumPaid == 1 {
paid = "ein bezahltes "
} else if bin.NumPaid > 1 {
paid = fmt.Sprint(bin.NumPaid, " bezahlte ")
}
if bin.NumFree > 0 && bin.NumPaid > 0 {
conjunction = "und "
}
if bin.NumPaid == 1 || bin.NumPaid == 0 && bin.NumFree == 1 { // match number of adjective immediately before noun
noun = "Produkt "
} else {
noun = "Produkte "
}
if category != "" {
preposition = `aus der Kategorie "`
quote = `" `
}
if totalEuros, totalCents, err := parsePrice(order.Total); err == nil && (totalEuros > 0 || totalCents > 0) {
var adverb, fraction string
if bin.NumPaid > 1 {
adverb = "insgesamt "
}
if totalCents > 0 {
fraction = fmt.Sprintf(",%02d", totalCents)
}
total = fmt.Sprint("für ", adverb, totalEuros, fraction, " Euro ")
}
body.WriteString(fmt.Sprint(adverb, verb, free, conjunction, paid, noun, preposition, category, quote, total, "bestellt."))
adverb = adverbs[rng.Intn(len(adverbs))]
} }
message := event.MessageEventContent{ message := event.MessageEventContent{
MsgType: event.MsgText, MsgType: event.MsgText,
Body: fmt.Sprint("\U0001f389 Es ", verb, free, conjunction, paid, noun, total, "bestellt."), Body: body.String(),
} }
success := s.matrix.Broadcast(&message) success := s.matrix.Broadcast(&message)
@ -159,6 +182,50 @@ func (s Server) orderPlaced(w http.ResponseWriter, r *http.Request) {
} }
} }
func (s Server) fetchCategory(organizer, event string, item uint) string {
resp, err := s.doGetRequest(s.buildURL(urlItem, organizer, event, item))
if err != nil {
return ""
}
defer resp.Body.Close()
if resp.StatusCode != 200 || resp.Header.Get("Content-Type") != "application/json" {
return ""
}
decoder := json.NewDecoder(resp.Body)
itemData := struct{
Category uint
}{}
err = decoder.Decode(&itemData)
if err != nil || itemData.Category == 0 {
return ""
}
resp, err = s.doGetRequest(s.buildURL(urlItemCategory, organizer, event, itemData.Category))
if err != nil {
return ""
}
defer resp.Body.Close()
if resp.StatusCode != 200 || resp.Header.Get("Content-Type") != "application/json" {
return ""
}
decoder = json.NewDecoder(resp.Body)
itemCategory := struct{
Name string
}{}
err = decoder.Decode(&itemCategory)
if err != nil {
return ""
}
return itemCategory.Name
}
func parsePrice(price string) (euros uint64, cents uint64, err error) { func parsePrice(price string) (euros uint64, cents uint64, err error) {
parts := strings.Split(price, ".") parts := strings.Split(price, ".")

View File

@ -3,11 +3,9 @@ package pretix
import ( import (
"context" "context"
"database/sql" "database/sql"
"fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
"strings"
"sync" "sync"
"git.luj0ga.de/franconian/matrix" "git.luj0ga.de/franconian/matrix"
@ -96,11 +94,6 @@ func (s Server) createTables() error {
return nil return nil
} }
func (s Server) buildURL(endpoint string, args ...any) string {
baseURL := strings.TrimSuffix(s.config.BaseURL, "/")
return fmt.Sprintf(endpoint, append([]any{baseURL}, args...)...)
}
func writeStatus(w http.ResponseWriter, code int) { func writeStatus(w http.ResponseWriter, code int) {
w.WriteHeader(code) w.WriteHeader(code)
io.WriteString(w, http.StatusText(code)) io.WriteString(w, http.StatusText(code))