merchant.go (11452B)
1 package merchant 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 11 "github.com/schanzen/taler-go/pkg/util" 12 ) 13 14 type MerchantConfig struct { 15 // Default currency 16 Currency string `json:"currency"` 17 18 // Supported currencies 19 Currencies map[string]util.CurrencySpecification `json:"currencies"` 20 21 // Name 22 Name string `json:"name"` 23 24 // Version string 25 Version string `json:"version"` 26 } 27 28 type PostOrderRequest struct { 29 // The order must at least contain the minimal 30 // order detail, but can override all. 31 Order CommonOrder `json:"order"` 32 33 // If set, the backend will then set the refund deadline to the current 34 // time plus the specified delay. If it's not set, refunds will not be 35 // possible. 36 RefundDelay int64 `json:"refund_delay,omitempty"` 37 38 // Specifies the payment target preferred by the client. Can be used 39 // to select among the various (active) wire methods supported by the instance. 40 PaymentTarget string `json:"payment_target,omitempty"` 41 42 // Specifies that some products are to be included in the 43 // order from the inventory. For these inventory management 44 // is performed (so the products must be in stock) and 45 // details are completed from the product data of the backend. 46 // FIXME: Not sure we actually need this for now 47 //InventoryProducts []MinimalInventoryProduct `json:"inventory_products,omitempty"` 48 49 // Specifies a lock identifier that was used to 50 // lock a product in the inventory. Only useful if 51 // inventory_products is set. Used in case a frontend 52 // reserved quantities of the individual products while 53 // the shopping cart was being built. Multiple UUIDs can 54 // be used in case different UUIDs were used for different 55 // products (i.e. in case the user started with multiple 56 // shopping sessions that were combined during checkout). 57 LockUuids []string `json:"lock_uuids,omitempty"` 58 59 // Should a token for claiming the order be generated? 60 // False can make sense if the ORDER_ID is sufficiently 61 // high entropy to prevent adversarial claims (like it is 62 // if the backend auto-generates one). Default is 'true'. 63 CreateToken bool `json:"create_token,omitempty"` 64 } 65 66 type CommonOrder struct { 67 // Total price for the transaction. The exchange will subtract deposit 68 // fees from that amount before transferring it to the merchant. 69 Amount string `json:"amount"` 70 71 // Maximum total deposit fee accepted by the merchant for this contract. 72 // Overrides defaults of the merchant instance. 73 MaxFee string `json:"max_fee,omitempty"` 74 75 // Human-readable description of the whole purchase. 76 Summary string `json:"summary"` 77 78 // Map from IETF BCP 47 language tags to localized summaries. 79 SummaryI18n string `json:"summary_i18n,omitempty"` 80 81 // Unique identifier for the order. Only characters 82 // allowed are "A-Za-z0-9" and ".:_-". 83 // Must be unique within a merchant instance. 84 // For merchants that do not store proposals in their DB 85 // before the customer paid for them, the order_id can be used 86 // by the frontend to restore a proposal from the information 87 // encoded in it (such as a short product identifier and timestamp). 88 OrderId string `json:"order_id,omitempty"` 89 90 // URL where the same contract could be ordered again (if 91 // available). Returned also at the public order endpoint 92 // for people other than the actual buyer (hence public, 93 // in case order IDs are guessable). 94 PublicReorderUrl string `json:"public_reorder_url,omitempty"` 95 96 // See documentation of fulfillment_url field in ContractTerms. 97 // Either fulfillment_url or fulfillment_message must be specified. 98 // When creating an order, the fulfillment URL can 99 // contain ${ORDER_ID} which will be substituted with the 100 // order ID of the newly created order. 101 FulfillmentUrl string `json:"fulfillment_url,omitempty"` 102 103 // See documentation of fulfillment_message in ContractTerms. 104 // Either fulfillment_url or fulfillment_message must be specified. 105 FulfillmentMessage string `json:"fulfillment_message,omitempty"` 106 107 // Map from IETF BCP 47 language tags to localized fulfillment 108 // messages. 109 FulfillmentMessageI18n string `json:"fulfillment_message_i18n,omitempty"` 110 111 // Minimum age the buyer must have to buy. 112 MinimumAge *uint64 `json:"minimum_age,omitempty"` 113 114 // List of products that are part of the purchase. 115 //products?: Product[]; 116 117 // Time when this contract was generated. If null, defaults to current 118 // time of merchant backend. 119 Timestamp *uint64 `json:"timestamp,omitempty"` 120 121 // After this deadline has passed, no refunds will be accepted. 122 // Overrides deadline calculated from refund_delay in 123 // PostOrderRequest. 124 RefundDeadline *uint64 `json:"refund_deadline,omitempty"` 125 126 // After this deadline, the merchant won't accept payments for the contract. 127 // Overrides deadline calculated from default pay delay configured in 128 // merchant backend. 129 PayDeadline *uint64 `json:"pay_deadline,omitempty"` 130 131 // Transfer deadline for the exchange. Must be in the deposit permissions 132 // of coins used to pay for this order. 133 // Overrides deadline calculated from default wire transfer delay 134 // configured in merchant backend. Must be after refund deadline. 135 WireTransferDeadline *uint64 `json:"wire_transfer_deadline,omitempty"` 136 137 // Base URL of the (public!) merchant backend API. 138 // Must be an absolute URL that ends with a slash. 139 // Defaults to the base URL this request was made to. 140 MerchantBaseUrl string `json:"merchant_base_url,omitempty"` 141 142 // Delivery location for (all!) products. 143 //DeliveryLocation?: Location; 144 145 // Time indicating when the order should be delivered. 146 // May be overwritten by individual products. 147 // Must be in the future. 148 DeliveryDate *uint64 `json:"delivery_deadline,omitempty"` 149 150 // See documentation of auto_refund in ContractTerms. 151 // Specifies for how long the wallet should try to get an 152 // automatic refund for the purchase. 153 AutoRefund *uint64 `json:"auto_refund,omitempty"` 154 155 // Extra data that is only interpreted by the merchant frontend. 156 // Useful when the merchant needs to store extra information on a 157 // contract without storing it separately in their database. 158 // Must really be an Object (not a string, integer, float or array). 159 Extra string 160 } 161 162 // NOTE: Part of the above but optional 163 type FulfillmentMetadata struct { 164 // See documentation of fulfillment_url in ContractTerms. 165 // Either fulfillment_url or fulfillment_message must be specified. 166 FulfillmentUrl string `json:"fulfillment_url,omitempty"` 167 168 // See documentation of fulfillment_message in ContractTerms. 169 // Either fulfillment_url or fulfillment_message must be specified. 170 FulfillmentMessage string `json:"fulfillment_message,omitempty"` 171 } 172 173 type PostOrderResponse struct { 174 // Order ID of the response that was just created. 175 OrderId string `json:"order_id"` 176 } 177 178 type PostOrderResponseToken struct { 179 // Token that authorizes the wallet to claim the order. 180 // Provided only if "create_token" was set to 'true' 181 // in the request. 182 Token string 183 } 184 185 type CheckPaymentStatusResponse struct { 186 // Status of the order 187 OrderStatus string `json:"order_status"` 188 } 189 190 type CheckPaymentPaytoResponse struct { 191 // Status of the order 192 TalerPayUri string `json:"taler_pay_uri"` 193 } 194 195 type Merchant struct { 196 197 // The host of this merchant 198 BaseUrlPrivate string 199 200 // The access token to use for the private API 201 AccessToken string 202 } 203 204 func NewMerchant(merchBaseUrlPrivate string, merchAccessToken string) Merchant { 205 return Merchant{ 206 BaseUrlPrivate: merchBaseUrlPrivate, 207 AccessToken: merchAccessToken, 208 } 209 } 210 211 type PaymentStatus string 212 213 const ( 214 OrderStatusUnknown = "" 215 OrderPaid = "paid" 216 OrderUnpaid = "unpaid" 217 OrderClaimed = "claimed" 218 ) 219 220 func (m *Merchant) IsOrderPaid(orderId string) (int, PaymentStatus, string, error) { 221 var orderPaidResponse CheckPaymentStatusResponse 222 var paytoResponse CheckPaymentPaytoResponse 223 client := &http.Client{} 224 req, _ := http.NewRequest("GET", m.BaseUrlPrivate+"/private/orders/"+orderId, nil) 225 req.Header.Set("Authorization", "Bearer secret-token:"+m.AccessToken) 226 resp, err := client.Do(req) 227 if nil != err { 228 return resp.StatusCode, OrderStatusUnknown, "", err 229 } 230 defer resp.Body.Close() 231 if http.StatusOK != resp.StatusCode { 232 message := fmt.Sprintf("Expected response code %d. Got %d", http.StatusOK, resp.StatusCode) 233 return resp.StatusCode, OrderStatusUnknown, "", errors.New(message) 234 } 235 respData, err := io.ReadAll(resp.Body) 236 if err != nil { 237 return resp.StatusCode, OrderStatusUnknown, "", err 238 } 239 err = json.NewDecoder(bytes.NewReader(respData)).Decode(&orderPaidResponse) 240 if err != nil { 241 return resp.StatusCode, OrderStatusUnknown, "", err 242 } 243 if orderPaidResponse.OrderStatus == "unpaid" { 244 err = json.NewDecoder(bytes.NewReader(respData)).Decode(&paytoResponse) 245 return resp.StatusCode, PaymentStatus(orderPaidResponse.OrderStatus), paytoResponse.TalerPayUri, err 246 } 247 return resp.StatusCode, PaymentStatus(orderPaidResponse.OrderStatus), "", nil 248 } 249 250 func (m *Merchant) GetConfig() (*MerchantConfig, error) { 251 var configResponse MerchantConfig 252 client := &http.Client{} 253 req, _ := http.NewRequest("GET", m.BaseUrlPrivate+"/config", nil) 254 resp, err := client.Do(req) 255 if nil != err { 256 return nil, err 257 } 258 defer resp.Body.Close() 259 if http.StatusOK != resp.StatusCode { 260 message := fmt.Sprintf("Expected response code %d. Got %d", http.StatusOK, resp.StatusCode) 261 return nil, errors.New(message) 262 } 263 respData, err := io.ReadAll(resp.Body) 264 if err != nil { 265 return nil, err 266 } 267 err = json.NewDecoder(bytes.NewReader(respData)).Decode(&configResponse) 268 if err != nil { 269 return nil, err 270 } 271 return &configResponse, nil 272 } 273 274 func (m *Merchant) CreateOrder(order CommonOrder) (string, error) { 275 var newOrder PostOrderRequest 276 var orderResponse PostOrderResponse 277 newOrder.Order = order 278 reqString, err := json.Marshal(newOrder) 279 if nil != err { 280 return "", err 281 } 282 client := &http.Client{} 283 req, _ := http.NewRequest(http.MethodPost, m.BaseUrlPrivate+"/private/orders", bytes.NewReader(reqString)) 284 req.Header.Set("Authorization", "Bearer secret-token:"+m.AccessToken) 285 resp, err := client.Do(req) 286 287 if nil != err { 288 return "", err 289 } 290 defer resp.Body.Close() 291 if http.StatusOK != resp.StatusCode { 292 message := fmt.Sprintf("Expected response code %d. Got %d. With request %s", http.StatusOK, resp.StatusCode, reqString) 293 return "", errors.New(message) 294 } 295 err = json.NewDecoder(resp.Body).Decode(&orderResponse) 296 return orderResponse.OrderId, err 297 } 298 299 func (m *Merchant) AddNewOrder(cost util.Amount, summary string, fulfillment_url string) (string, error) { 300 var newOrder PostOrderRequest 301 var orderDetail CommonOrder 302 var orderResponse PostOrderResponse 303 orderDetail.Amount = cost.String() 304 // FIXME get from cfg 305 orderDetail.Summary = summary 306 orderDetail.FulfillmentUrl = fulfillment_url 307 newOrder.Order = orderDetail 308 reqString, err := json.Marshal(newOrder) 309 if nil != err { 310 return "", err 311 } 312 client := &http.Client{} 313 req, _ := http.NewRequest(http.MethodPost, m.BaseUrlPrivate+"/private/orders", bytes.NewReader(reqString)) 314 req.Header.Set("Authorization", "Bearer secret-token:"+m.AccessToken) 315 resp, err := client.Do(req) 316 317 if nil != err { 318 return "", err 319 } 320 defer resp.Body.Close() 321 if http.StatusOK != resp.StatusCode { 322 message := fmt.Sprintf("Expected response code %d. Got %d. With request %s", http.StatusOK, resp.StatusCode, reqString) 323 return "", errors.New(message) 324 } 325 err = json.NewDecoder(resp.Body).Decode(&orderResponse) 326 return orderResponse.OrderId, err 327 }