taldir

Directory service to resolve wallet mailboxes by messenger addresses
Log | Files | Refs | Submodules | README | LICENSE

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 }