donations.go (7291B)
1 package web 2 3 import ( 4 "net/http" 5 "net/url" 6 "strconv" 7 "strings" 8 "time" 9 10 "git.taler.net/taler-merchant-demos/internal/backend" 11 ) 12 13 type donationsIndexContent struct{ Currency string } 14 15 type checkoutContent struct { 16 Currency string 17 Receiver string 18 Amount string 19 Donor string 20 } 21 22 type fulfillmentContent struct { 23 Receiver string 24 Amount string 25 Donor string 26 OrderID string 27 RequestURL string 28 } 29 30 type providerNotSupportedContent struct { 31 BackURL string 32 BackToCheckout bool 33 } 34 35 func (a *App) registerDonationRoutes() { 36 a.handleGet("/{lang}/{$}", a.donationsIndex) 37 a.handleGet("/{lang}/checkout", a.donationCheckout) 38 a.handleGet("/{lang}/provider-not-supported", a.donationProviderNotSupported) 39 a.handleGet("/{lang}/donate", a.donate) 40 a.handleGet("/{lang}/donation/{receiver}", a.donationFulfillment) 41 } 42 43 func (a *App) donationLanguage(w http.ResponseWriter, r *http.Request) (string, bool) { 44 lang := r.PathValue("lang") 45 if !validLocale(lang) { 46 a.renderError(w, r, http.StatusNotFound, "en", "Page not found", nil) 47 return "", false 48 } 49 return lang, true 50 } 51 52 func (a *App) donationsIndex(w http.ResponseWriter, r *http.Request) { 53 lang, ok := a.donationLanguage(w, r) 54 if !ok { 55 return 56 } 57 a.render(w, "donations-index", a.makePage(r, lang, "GNU Taler Demo: Donations", donationsIndexContent{a.opts.Currency}), http.StatusOK) 58 } 59 60 func (a *App) donationProviderNotSupported(w http.ResponseWriter, r *http.Request) { 61 lang, ok := a.donationLanguage(w, r) 62 if !ok { 63 return 64 } 65 prefix := forwardedPrefix(r) + "/" + url.PathEscape(lang) 66 query := r.URL.Query() 67 receiver := strings.TrimSpace(query.Get("donation_receiver")) 68 amount := strings.TrimSpace(query.Get("donation_amount")) 69 donor := strings.TrimSpace(query.Get("donation_donor")) 70 content := providerNotSupportedContent{BackURL: prefix + "/"} 71 if receiver != "" && amount != "" && donor != "" { 72 values := url.Values{ 73 "donation_receiver": {receiver}, 74 "donation_amount": {amount}, 75 "donation_donor": {donor}, 76 } 77 content.BackURL = prefix + "/checkout?" + values.Encode() 78 content.BackToCheckout = true 79 } 80 a.render(w, "donations-provider", a.makePage(r, lang, "GNU Taler Demo: Donations", content), http.StatusOK) 81 } 82 83 func (a *App) requiredQuery(w http.ResponseWriter, r *http.Request, lang, name string) (string, bool) { 84 value := strings.TrimSpace(r.URL.Query().Get(name)) 85 if value == "" { 86 a.renderErrorf(w, r, http.StatusBadRequest, lang, "Parameter {name} is required.", nil, "name", name) 87 return "", false 88 } 89 return value, true 90 } 91 92 func (a *App) donationCheckout(w http.ResponseWriter, r *http.Request) { 93 lang, ok := a.donationLanguage(w, r) 94 if !ok { 95 return 96 } 97 receiver, ok := a.requiredQuery(w, r, lang, "donation_receiver") 98 if !ok { 99 return 100 } 101 amount, ok := a.requiredQuery(w, r, lang, "donation_amount") 102 if !ok { 103 return 104 } 105 donor, ok := a.requiredQuery(w, r, lang, "donation_donor") 106 if !ok { 107 return 108 } 109 content := checkoutContent{Currency: a.opts.Currency, Receiver: receiver, Amount: amount, Donor: donor} 110 a.render(w, "donations-checkout", a.makePage(r, lang, "GNU Taler Demo: Donations checkout", content), http.StatusOK) 111 } 112 113 func (a *App) donate(w http.ResponseWriter, r *http.Request) { 114 lang, ok := a.donationLanguage(w, r) 115 if !ok { 116 return 117 } 118 receiver, ok := a.requiredQuery(w, r, lang, "donation_receiver") 119 if !ok { 120 return 121 } 122 client := a.opts.DonationBackends[receiver] 123 if client == nil { 124 a.renderError(w, r, http.StatusBadRequest, lang, "Unknown donation receiver", nil) 125 return 126 } 127 amount, ok := a.requiredQuery(w, r, lang, "donation_amount") 128 if !ok { 129 return 130 } 131 donor, ok := a.requiredQuery(w, r, lang, "donation_donor") 132 if !ok { 133 return 134 } 135 paymentSystem, ok := a.requiredQuery(w, r, lang, "payment_system") 136 if !ok { 137 return 138 } 139 if paymentSystem != "taler" { 140 query := url.Values{ 141 "donation_receiver": {receiver}, 142 "donation_amount": {amount}, 143 "donation_donor": {donor}, 144 } 145 target := forwardedPrefix(r) + "/" + url.PathEscape(lang) + "/provider-not-supported?" + query.Encode() 146 http.Redirect(w, r, target, http.StatusFound) 147 return 148 } 149 fulfillmentURL := externalOrigin(r) + forwardedPrefix(r) + "/" + url.PathEscape(lang) + "/donation/" + url.PathEscape(receiver) + "?timestamp=" + strconv.FormatInt(time.Now().UnixNano(), 10) + "&order_id=${ORDER_ID}" 150 order := backend.Order{ 151 Extra: backend.OrderExtra{Donor: donor, Receiver: receiver, Amount: amount}, 152 FulfillmentURL: fulfillmentURL, 153 // Translators: Merchant order summary shown in the wallet; {receiver} is the project receiving the donation. 154 Summary: formatNamed(a.catalogs.translate(lang, "Donation to {receiver}"), "receiver", receiver), 155 PayDeadline: backend.Timestamp{Seconds: time.Now().Add(10 * time.Minute).Unix()}, 156 WireTransferDeadline: backend.Timestamp{Seconds: time.Now().Add(30 * time.Minute).Unix()}, 157 MinimumAge: 16, 158 } 159 if a.opts.DonauURL == "" { 160 order.Amount = amount 161 } else { 162 order.Version = 1 163 // Translators: Merchant order summary shown in the wallet; {receiver} is the project receiving the donation. 164 order.Summary = formatNamed(a.catalogs.translate(lang, "Donation to {receiver} (with receipt)"), "receiver", receiver) 165 order.Choices = []backend.OrderChoice{{ 166 Amount: amount, 167 Outputs: []backend.OrderOutput{{Type: "tax-receipt"}}, 168 }} 169 } 170 var response backend.PostOrderResponse 171 err := client.Post(r.Context(), "private/orders", backend.PostOrderRequest{Order: order}, &response) 172 if err != nil { 173 a.renderError(w, r, http.StatusBadGateway, lang, "Backend could not create the order", err) 174 return 175 } 176 createdOrderID := response.OrderID 177 if createdOrderID == "" { 178 a.renderError(w, r, http.StatusBadGateway, lang, "Backend response did not contain an order ID", nil) 179 return 180 } 181 target := forwardedPrefix(r) + "/" + url.PathEscape(lang) + "/donation/" + url.PathEscape(receiver) + "?order_id=" + url.QueryEscape(createdOrderID) 182 http.Redirect(w, r, target, http.StatusFound) 183 } 184 185 func (a *App) donationFulfillment(w http.ResponseWriter, r *http.Request) { 186 lang, ok := a.donationLanguage(w, r) 187 if !ok { 188 return 189 } 190 receiver := r.PathValue("receiver") 191 client := a.opts.DonationBackends[receiver] 192 if client == nil { 193 a.renderError(w, r, http.StatusBadRequest, lang, "Unknown donation receiver", nil) 194 return 195 } 196 requestedOrderID, ok := a.requiredQuery(w, r, lang, "order_id") 197 if !ok { 198 return 199 } 200 var payment backend.OrderStatusResponse 201 if err := client.Get(r.Context(), "private/orders/"+url.PathEscape(requestedOrderID), nil, &payment); err != nil { 202 a.renderError(w, r, http.StatusBadGateway, lang, "Backend request failed", err) 203 return 204 } 205 if payment.OrderStatus != "paid" { 206 target := payment.OrderStatusURL 207 if target == "" { 208 a.renderError(w, r, http.StatusBadGateway, lang, "Backend response did not contain an order status URL", nil) 209 return 210 } 211 http.Redirect(w, r, target, http.StatusFound) 212 return 213 } 214 extra := payment.ContractTerms.Extra 215 content := fulfillmentContent{ 216 Receiver: extra.Receiver, Amount: extra.Amount, 217 Donor: extra.Donor, OrderID: requestedOrderID, 218 RequestURL: externalURLWithQuery(r), 219 } 220 a.render(w, "donations-fulfillment", a.makePage(r, lang, "GNU Taler Demo: Donations", content), http.StatusOK) 221 }