The Promotably API Server
Make sure your ~/.lein/profiles.clj has at least:
$ cat ~/.lein/profiles.clj
{:user {:plugins [[lein-midje "3.0.0"]
[cider/cider-nrepl "0.8.0"]]}}
NOTE: If you are using the devbox/schmetterling repo for a development VM, you don't need to do this.
CREATE USER p_user WITH PASSWORD 'pr0m0';
CREATE DATABASE promotably_dev;
GRANT ALL PRIVILEGES ON DATABASE promotably_dev to p_user;
\c promotably_dev
CREATE EXTENSION "uuid-ossp";
CREATE TABLE migrations(version varchar(255));
GRANT ALL PRIVILEGES ON TABLE migrations TO p_user;
API uses drift for migrations.
lein migrate
lein create-migration <the name of the migration>
This places a migration file in the src/migrations directory
Before you can run the server or the integration tests you need AWS credentials.
Once you have an id and key you can save the credentials to ~/.aws/credentials
:
[promotably]
aws_access_key_id=<ID>
aws_secret_access_key=<KEY>
lein run
ENV=test lein midje "api.unit.*"
KINESIS_A=dev-PromotablyAPIEvents RDS_HOST=localhost RDS_PORT=5432 RDS_USER=p_user RDS_DB_NAME=promotably_dev RDS_PW=pr0m0 REDIS_HOST=localhost REDIS_PORT=6379 ENV=integration lein midje "api.integration.*"
Integration testing depends on AWS credentials as per doc.
Before you can begin testing conditions you will want to make sure you are not in the control group. First you need to generate a test ID:
lein test-uuid
Once you have this new uuid, open up your browser and edit the cookie promotably
for
api-staging.promotably.com
or whatever domain your are testing against.
Use the usual in emacs: cider-jack-in
. Open core.clj and load the file in cider. Then eval the desired lines at the bottom of the file:
(System/setProperty "ENV" "dev")
(System/setProperty "ENV" "localdev")
(System/getProperty "ENV")
(prn api.system/current-system)
(go {:port 3000 :repl-port 55555})
(stop)
Authenticated requests to the Promotably API include a cryptographic signature to allow the API to validate requests. The reqestor includes an HTTP header ('Promotably-Auth') that has been computed as follows.
First, the requestor computes a cryptographic hash (an HMAC) of the request body using the SHA1 algorithm.
body-hash = HEXDIGEST(HMAC(request body))
The requestor then computes a string of header lines to be included in the signature string. The included header lines are sorted alphabetically, and put in Header-Name: Header-Value + newline ("\n") form, and concatenated together. For example, if the requestor wants to include the User-Agent header and the Accept header, the string might be:
Accept: /\nUser-Agent: curl/7.10.6 (i386-redhat-linux-gnu) libcurl/7.10.6 OpenSSL/0.9.7a ipv6 zlib/1.1.4\n
Next, the requestor computes a string of query string fields to be included in the signature string. The included query string field names are sorted alphabetically, and put in field-name=field-value
ampersand ("&") form, and concatenated together. For example, if the requestor's URL looked like:
Then, the string might be:
event-name=_trackProductView&product-id=WW1&site-id=26b28c70-2144-4427-aee3-b51031b08426
Note that the requestor need not specify the totality of all query string fields available in the URL; nor any at all, for that matter.
Then the requestor computes a signature string by concatenating the following elements:
0. requestor's site id + newline ("\n")
1. requestor's API secret key + newline ("\n")
2. http host (as presented in HTTP request) + newline ("\n")
3. http verb (as presented in HTTP request) + newline ("\n")
4. request URL (excluding query string, and then percent-encoded) + newline ("\n")
5. datetime in iso8601 format (YYYYMMDDTHHMMSSZ) + newline ("\n")
6. body-hash + newline ("\n"). If no body is present, just newline.
7. signed-header-lines + newline ("\n")
8. signed-query-string-fields + newline ("\n")
A string hexdigest is computed from the signature string:
signature = HEXDIGEST(HMAC(signature-string))
The requestor then constructs a valid authorization value for use in the Promotably-Auth header by concatenating the following strings:
1. "hmac-sha1/" +
2. names (not values) of signed headers, sorted alphabetically and
separated by commas (","). Eg., "Accept,User-Agent"
3. "/"
4. names (not values) of signed query string fields, sorted alphabetically and
separated by commas (","). Eg., "event-name,product-id,site-id"
5. "/"
6. datetime in iso8601 format (YYYYMMDDTHHMMSSZ)
7. "/"
8. signature (as computed above)
The resulting string is then url encoded, and the value of the Promotably-Auth header is set to the result.
Alternatively, the requestor may append the authorization value as an additional query string parameter. Eg., "&promotably-auth=..." If so, the uri used in computation of the signature string (above) does not include the promotably-auth parameter. If transmitted in the query string, this query parameter must the be the last one present in the query string.
As shoppers explore and interact with our customers' e-commerce sites, their behvaior reveals intent and other information that Signal96 uses for product recommendation, dynamic promotion creation and display, and other uses. This shopper behavior is transmitted to our tracking API by a javascript library residing on the e-commerce site.
JSONP endpoint for tracking shopper interaction with customer e-commerce sites.
ENDPOINT: GET https://api.promotably.com/v1/track
AUTHENTICATION: signature
RESPONSE STATUS: 200
RESPONSE BODY: none
* site-id - Required. String. Identifies customer site.
* event-type - Required. String. Event name.
* <event type dependent> - See below.
Accepted event types include:
* _trackProductView
* _trackProductAdd
* _trackCartView
* _trackCheckout
* _trackThankYou
Accepted query string parameters include:
* shopper-id - Zero or one. String. Persistent ID of shopper, if known.
* product-id - Required. String.
* title - Required. String.
* short-description - Optional. String.
* modified-at - Optional. Viz http://goo.gl/yUwaVG, http://goo.gl/M45cmt
* description - Optional. String.
* variation[] - Zero or more. String. Value of each query param is
of the form "variation-id,variation-value". For
example: ...&variation[]=1,2&variation[]=2,9...
Accepted query string parameters include:
* shopper-id - Zero or one. String. Persistent ID of shopper, if known.
* product-id - Required. String.
* quantity - Required. Positive integer > 0 expressed as a string.
* variation[] - Zero or more. String. Value of each query param is
of the form "variation-id,variation-value". For
example: ...&variation[]=1,2&variation[]=2,9...
Accepted query string parameters include:
* shopper-id - Zero or one. String. Persistent ID of shopper, if known.
* cart-item[] - Zero or more. String. Value of each query
param is of the form "product-id,product-title,product-type,variation-id,variation,quantity"
For example: ...&cart-item[]=11,Widget,simple,0,0,1&cart-item[]=98,Thingie,simple,1,7,2...
Accepted query string parameters include:
* shopper-id - Zero or one. String. Persistent ID of shopper, if known.
* billing-address - Zero or one. String. Value of each query
param is of the form
"name,address-1,address-2,city,state,postcode,phone"
* shipping-address - Zero or one. String. Value of each query
param is of the form
"name,address-1,address-2,city,state,postcode,phone"
* applied-coupon[] - Zero or more. String.
* cart-item[] - Zero or more. String. Value of each query param
is of the form
"product-id,product-title,product-type,variation-id,variation,quantity".
For example:
...&cart-item[]=11,Widget,simple,0,0,1&cart-item[]=98,Thingie,simple,1,7,2...
Accepted query string parameters include:
* shopper-id - Zero or one. String. Persistent ID of shopper, if known.
* shopper-email - Zero or one. String.
* order-id - Required. String unique identifier for order.
* billing-address - Zero or one. String. Value of each query
param is of the form
"firstname,lastname,address-1,address-2,city,state,postcode,country,phone"
* billing-email - Zero or one. String.
* shipping-address - Zero or one. String. Value of each query
param is of the form
"name,address-1,address-2,city,state,postcode,phone"
* applied-coupon[] - Zero or more. String.
* cart-item[] - Zero or more. String. Value of each query param
is of the form
"product-id,product-title,product-type,variation-id,variation,quantity,line-subtotal,line-total".
For example:
...&cart-item[]=11,Widget,simple,0,0,1&cart-item[]=98,Thingie,simple,1,7,2,10.00,20.00...
Promotably augments our customers' e-commerce platforms with a robust, flexible and intuitive system for creating, managing, using and tracking promotions and coupons. Promotably integrations with customer e-commerce platforms are usually accomplished with plugins, which enhance (or sometimes replace) native e-commerce platform capabilities with Promotably's SaaS coupon system.
Our plugins call the Promotably Coupon API to query and validate coupons as well as to calculate discounts.
Called by e-commerce platform plugins (the server side component) to check if a given coupon exists, and to get its parameters.
ENDPOINT: GET https://api.signal96.com/v1/promos/query/
AUTHENTICATION: signature
RESPONSE STATUS: 200
RESPONSE BODY: JSON. See below.
* site-id - Required. String. Identifies customer site.
The response has the JSON form:
{ "amount":"20", "apply-before-tax": true, "code":"TWENTYOFF", "current-usage-count": 18, "exclude-product-categories":["underpants"], "exclude-product-ids":[] "exclude-sale-items":true, "expiry-date": "2007-01-31T23:59:59Z", "free-shipping": false, "id":"a2e628a8-1ef5-4efa-9b8b-ffdc99725485", "incept-date": "2006-01-31T23:59:59Z", "individual-use": true, "limit-usage-to-x-items":1, "max-usage-count": 100, "minimum-cart-amount": 100, "minimum-product-amount": 0, "product-categories":["widgets","thingies"], "product-ids":[1,34,99], "type":"percent_product", "usage-limit-per-user": -1, }
type: Enum. One of "percent_product", "amount_product", "percent_cart", "amount_cart".
minimum-product-amount: Float. If product-ids is not empty, to qualify for this promo, the product to which it applies must exceed this amount in value. If product-categories is not empty, the qualify for this promo, the cart must contain an item from an allowed category which exceeds this amount in value. If both of those fields are empty, the shopper's cart must have at least one item that exceeds this amount in value.
minimum-cart-amount: Float. The total value of the shopper's cart must exceed this amount in value to qualify for this promo.
TODO: Response should include nonce value for subsequent requests?
Called by e-commerce platform plugins (the server side component) to check if a given coupon is valid for use.
ENDPOINT: POST https://api.signal96.com/v1/promos/validation/
AUTHENTICATION: signature
REQUEST BODY: JSON. See below.
RESPONSE STATUS: 201
RESPONSE BODY: JSON. See below.
* site-id - Required. String. Identifies customer site.
The request has the JSON form:
{ "code":"TWENTYOFF", "shopper-id": "12-AB33", "shopper-email": "cvillecsteele@gmail.com", "applied-coupons":["FIFTYOFF","FREESHIP"], "cart-items":[ {"product-id":1, "product-title":"Widget", "product-type":"simple", "product-categories":["Hammers","Tools"], "variation-id":0, "variation":0, "quantity":2, "line-total":10, "line-subtotal":20, "line-tax":0, "line-subtotal-tax":0 }, {"product-id":10, "product-title":"Undies", "product-type":"simple", "product-categories":["Clothes","Unmentionables"], "variation-id":0, "variation":0, "quantity":1, "line-total":10, "line-subtotal":10, "line-tax":0, "line-subtotal-tax":0 } ], "product-ids-on-sale":[11,89] }
The response has the JSON form:
{ "code":"TWENTYOFF", "id":"a2e628a8-1ef5-4efa-9b8b-ffdc99725485", "valid":false, "message":"That coupon has expired." }
TODO: Do we need a variation of this endpoint for doing a validation against a particular product instead of the whole cart?
?
Called by e-commerce platform plugins (the server side component) to calculate the discount for a given coupon.
ENDPOINT: POST https://api.signal96.com/v1/promos/calculation/
AUTHENTICATION: signature
REQUEST BODY: JSON. See below.
RESPONSE STATUS: 201
RESPONSE BODY: JSON. See below.
* site-id - Required. String. Identifies customer site.
The request has the JSON form:
{ "code":"TWENTYOFF", "shopper-id": "12-AB33", "shopper-email": "cvillecsteele@gmail.com", "applied-coupons":["FIFTYOFF","FREESHIP"], "cart-items":[ {"product-id":1, "product-title":"Widget", "product-type":"simple", "product-categories":["Hammers","Tools"], "variation-id":0, "variation":0, "quantity":2, "line-total":10, "line-subtotal":20, "line-tax":0, "line-subtotal-tax":0 }, {"product-id":10, "product-title":"Undies", "product-type":"simple", "product-categories":["Clothes","Unmentionables"], "variation-id":0, "variation":0, "quantity":1, "line-total":10, "line-subtotal":10, "line-tax":0, "line-subtotal-tax":0 } ], "product-ids-on-sale":[11,89] }
The response has the JSON form:
{ "code":"TWENTYOFF", "id":"a2e628a8-1ef5-4efa-9b8b-ffdc99725485", "valid":true, "message":"Thanks for shopping!", "discount-amount": 2.00, "discounted-item":true, "discounted-line":false, "discounted-cart":false, "discounted-product-id":1, "number-discounted-items":1 }
TODO: Need to tighten up definition and verbiage around cart items, products, line items, etc...
Copyright © 2014 Promotably