Skip to content

Commit

Permalink
Merge pull request #37 from ripe-tech/jc/feat-migrate-to-new-ups-api
Browse files Browse the repository at this point in the history
feat: migrate to new ups api
  • Loading branch information
3rdvision authored Nov 9, 2023
2 parents 48934d0 + bf5cc4a commit 606f2f5
Show file tree
Hide file tree
Showing 13 changed files with 152 additions and 337 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

* Remove Travis CI - [products/#97](https://github.com/ripe-tech/products/issues/97)
* [BREAKING] Swap to new UPS client credentials OAuth and APIs - [#36](https://github.com/ripe-tech/ups-api-js/issues/36)

### Fixed

Expand Down
26 changes: 12 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ const upsApi = require("ups-api");

// instance the API client with defaults
const api = new upsApi.API({
username: "myupsaccount",
password: "myupsaccountpassword",
license: "3F9955C3F789C255"
clientId: "myUpsAppClientId",
clientSecret: "myUpsAppClientSecret"
});

// example: request tracking information
Expand All @@ -18,17 +17,16 @@ const tracking = await api.getTrackingDetails("7798339175");

## Configuration

| Name | Type | Default | Description |
| ----------------------------- | ----- | ----------------------------------------------------- | ----------------------------------------------------------------------------- |
| **UPS_DOCUMENT_BASE_URL** | `str` | `https://filexfer.ups.com/rest/PaperlessDocumentAPI/` | The base URL that is going to be used for Paperless Document API connections. |
| **UPS_LOCATOR_BASE_URL** | `str` | `https://onlinetools.ups.com/ups.app/xml/Locator/` | The base URL that is going to be used for Locator API connections. |
| **UPS_PICKUP_BASE_URL** | `str` | `https://onlinetools.ups.com/ship/v1707/pickups/` | The base URL that is going to be used for Pickup API connections. |
| **UPS_SHIPPING_BASE_URL** | `str` | `https://onlinetools.ups.com/ship/v1807/` | The base URL that is going to be used for Shipping API connections. |
| **UPS_TRACKING_BASE_URL** | `str` | `https://onlinetools.ups.com/track/v1/` | The base URL that is going to be used for Tracking API connections. |
| **UPS_TRACKING_XML_BASE_URL** | `str` | `https://onlinetools.ups.com/ups.app/xml/Track/` | The base URL that is going to be used for Tracking XML-based API connections. |
| **UPS_LICENSE** | `str` | `None` | The UPS API license to be used for authentication. |
| **UPS_USERNAME** | `str` | `None` | The UPS API username to be used for authentication |
| **UPS_PASSWORD** | `str` | `None` | The UPS API password to be used for authentication |
| Name | Type | Default | Description |
| ----------------------- | ----- | ------------------------------------ | ------------------------------------------------------ |
| **UPS_AUTH_URL** | `str` | `"https://onlinetools.ups.com/"` | The base auth URL used for the OAuth token request. |
| **UPS_BASE_URL** | `str` | `"https://onlinetools.ups.com/api/"` | The base URL used for API requests. |
| **UPS_API_VERSION** | `str` | `"v1"` | The version of the API to use. |
| **UPS_CLIENT_ID** | `str` | `None` | The application client ID to obtain the token. |
| **UPS_CLIENT_SECRET** | `str` | `None` | The application client secret to obtain the token. |
| **UPS_GRANT_TYPE** | `str` | `"client_credentials"` | The application grant type to obtain the token. |
| **UPS_TOKEN** | `str` | `None` | The token granted by the OAuth request. |
| **UPS_TRANSACTION_SRC** | `str` | `None` | The transaction source to be added to request headers. |

## License

Expand Down
181 changes: 70 additions & 111 deletions js/base.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,57 @@
import { API as BaseAPI, mix, load, conf, verify } from "yonius";

import { DocumentAPI } from "./document";
import { LocatorAPI } from "./locator";
import { PaperlessAPI } from "./paperless";
import { PickupAPI } from "./pickup";
import { ShipmentAPI } from "./shipment";
import { TrackingAPI } from "./tracking";

const DOCUMENT_BASE_URL = "https://filexfer.ups.com/rest/PaperlessDocumentAPI/";
const LOCATOR_BASE_URL = "https://onlinetools.ups.com/ups.app/xml/Locator/";
const PICKUP_BASE_URL = "https://onlinetools.ups.com/ship/v1707/pickups/";
const SHIPPING_BASE_URL = "https://onlinetools.ups.com/ship/v1/";
const TRACKING_BASE_URL = "https://onlinetools.ups.com/track/v1/";
const TRACKING_XML_BASE_URL = "https://onlinetools.ups.com/ups.app/xml/Track/";
/**
* The base auth URL used for the OAuth token request.
*/
const AUTH_URL = "https://onlinetools.ups.com/";

/**
* The base URL used for API requests.
*/
const BASE_URL = "https://onlinetools.ups.com/api/";

/**
* The version of the API to use.
*/
const API_VERSION = "v1";

/**
* The application grant type to obtain the token.
*/
const GRANT_TYPE = "client_credentials";

export class API extends mix(BaseAPI).with(
DocumentAPI,
LocatorAPI,
PaperlessAPI,
PickupAPI,
ShipmentAPI,
TrackingAPI
) {
constructor(kwargs = {}) {
super(kwargs);
this.documentBaseUrl = conf("UPS_DOCUMENT_BASE_URL", DOCUMENT_BASE_URL);
this.locatorBaseUrl = conf("UPS_LOCATOR_BASE_URL", LOCATOR_BASE_URL);
this.pickupBaseUrl = conf("UPS_PICKUP_BASE_URL", PICKUP_BASE_URL);
this.shippingBaseUrl = conf("UPS_SHIPPING_BASE_URL", SHIPPING_BASE_URL);
this.trackingBaseUrl = conf("UPS_TRACKING_BASE_URL", TRACKING_BASE_URL);
this.trackingXmlBaseUrl = conf("UPS_TRACKING_XML_BASE_URL", TRACKING_XML_BASE_URL);
this.license = conf("UPS_LICENSE", null);
this.username = conf("UPS_USERNAME", null);
this.password = conf("UPS_PASSWORD", null);

this.authUrl = conf("UPS_AUTH_URL", AUTH_URL);
this.baseUrl = conf("UPS_BASE_URL", BASE_URL);
this.version = conf("UPS_API_VERSION", API_VERSION);
this.clientId = conf("UPS_CLIENT_ID", null);
this.clientSecret = conf("UPS_CLIENT_SECRET", null);
this.grantType = conf("UPS_GRANT_TYPE", GRANT_TYPE);
this.token = conf("UPS_TOKEN", null);
this.transactionSrc = conf("UPS_TRANSACTION_SRC", null);
this.documentBaseUrl =
kwargs.documentBaseUrl === undefined ? this.documentBaseUrl : kwargs.documentBaseUrl;
this.locatorBaseUrl =
kwargs.locatorBaseUrl === undefined ? this.locatorBaseUrl : kwargs.locatorBaseUrl;
this.pickupBaseUrl =
kwargs.pickupBaseUrl === undefined ? this.pickupBaseUrl : kwargs.pickupBaseUrl;
this.shippingBaseUrl =
kwargs.shippingBaseUrl === undefined ? this.shippingBaseUrl : kwargs.shippingBaseUrl;
this.trackingBaseUrl =
kwargs.trackingBaseUrl === undefined ? this.trackingBaseUrl : kwargs.trackingBaseUrl;
this.trackingXmlBaseUrl =
kwargs.trackingXmlBaseUrl === undefined
? this.trackingXmlBaseUrl
: kwargs.trackingXmlBaseUrl;
this.license = kwargs.license === undefined ? this.license : kwargs.license;
this.username = kwargs.username === undefined ? this.username : kwargs.username;
this.password = kwargs.password === undefined ? this.password : kwargs.password;

this.authUrl = kwargs.authUrl === undefined ? this.authUrl : kwargs.authUrl;
this.baseUrl = kwargs.baseUrl === undefined ? this.baseUrl : kwargs.baseUrl;
this.version = kwargs.version === undefined ? this.version : kwargs.version;
this.clientId = kwargs.clientId === undefined ? this.clientId : kwargs.clientId;
this.clientSecret =
kwargs.clientSecret === undefined ? this.clientSecret : kwargs.clientSecret;
this.token = kwargs.token === undefined ? this.token : kwargs.token;
this.transactionSrc =
kwargs.transactionSrc === undefined ? this.transactionSrc : kwargs.transactionSrc;
}
Expand All @@ -59,37 +62,39 @@ export class API extends mix(BaseAPI).with(

async build(method, url, options = {}) {
await super.build(method, url, options);
options.kwargs = options.kwargs !== undefined ? options.kwargs : {};
const auth = options.kwargs.auth;
delete options.kwargs.auth;

// determines the kind of authentication method to be used and
// changes the corresponding header or data payload
switch (auth) {
case "headers":
options.headers = options.params !== undefined ? options.headers : {};
options.headers.AccessLicenseNumber = this.license;
if (this.username) options.headers.Username = this.username;
if (this.password) options.headers.Password = this.password;
break;
case "dataJ":
options.dataJ = options.dataJ !== undefined ? options.dataJ : {};
options.dataJ.UPSSecurity = options.dataJ.UPSSecurity || {
UsernameToken: {
Username: this.username,
Password: this.password
},
ServiceAccessToken: {
AccessLicenseNumber: this.license
}
};
break;
}

const transactionSrc = options.headers.transactionSrc || this.transactionSrc;
if (transactionSrc) options.headers.transactionSrc = transactionSrc;
}

async authCallback(params, headers) {
// forces the refetch of the authorization
// token from the auth API
this.token = null;
await this.getToken();

headers.Authorization = this._bearerHeader();
}

async getToken() {
if (this.token) return this.token;

const url = `${this.authUrl}security/${this.version}/oauth/token`;
const data = `grant_type=${this.grantType}`;
const options = {
headers: {
Authorization: this._basicHeader()
},
data: data,
mime: "application/x-www-form-urlencoded"
};

const contents = await this.post(url, options);
this.token = contents.access_token;

return this.token;
}

async _handleResponse(response, errorMessage = "Problem in request") {
const result = await this._getResult(response);
const errors =
Expand Down Expand Up @@ -129,58 +134,12 @@ export class API extends mix(BaseAPI).with(
return result;
}

/**
* Retrieves the document base URL, normalizing it according to
* the limitations of the UPS API.
*
* @returns {String} The normalized document base URL ready to be used by API calls.
*/
_getDocumentBaseUrl() {
// removes the trailing slash, as the API doesn't handle it properly
return this.documentBaseUrl.slice(0, this.documentBaseUrl.length - 1);
_basicHeader() {
const auth = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64");
return `Basic ${auth}`;
}

/**
* Retrieves the locator base URL, normalizing it according to
* the limitations of the UPS API.
*
* @returns {String} The normalized locator base URL ready to be used by API calls.
*/
_getLocatorBaseUrl() {
// removes the trailing slash, as the API doesn't handle it properly
return this.locatorBaseUrl.slice(0, this.locatorBaseUrl.length - 1);
}

/**
* Retrieves the pickup base URL, normalizing it according to
* the limitations of the UPS API.
*
* @returns {String} The normalized pickup base URL ready to be used by API calls.
*/
_getPickupBaseUrl() {
// removes the trailing slash, as the API doesn't handle it properly
return this.pickupBaseUrl.slice(0, this.pickupBaseUrl.length - 1);
}

/**
* Retrieves the tracking base URL, normalizing it according to
* the limitations of the UPS API.
*
* @returns {String} The normalized tracking base URL ready to be used by API calls.
*/
_getTrackingBaseUrl() {
// removes the trailing slash, as the API doesn't handle it properly
return this.trackingBaseUrl.slice(0, this.trackingBaseUrl.length - 1);
}

/**
* Retrieves the tracking XML base URL, normalizing it according to
* the limitations of the UPS API.
*
* @returns {String} The normalized tracking XML base URL ready to be used by API calls.
*/
_getTrackingXmlBaseUrl() {
// removes the trailing slash, as the API doesn't handle it properly
return this.trackingXmlBaseUrl.slice(0, this.trackingXmlBaseUrl.length - 1);
_bearerHeader() {
return `Bearer ${this.token}`;
}
}
3 changes: 1 addition & 2 deletions js/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export * from "./base";
export * from "./document";
export * from "./locator";
export * from "./paperless";
export * from "./pickup";
export * from "./shipment";
export * from "./tracking";
export * from "./util";
85 changes: 38 additions & 47 deletions js/locator.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { getXMLHeader, xmlToJson } from "./util";
/**
* Option type for access point search.
*/
export const ACCESS_POINT_SEARCH = "64";

/**
* Option type to sort by closest in search.
*/
export const CLOSEST_POINT_SEARCH = "01";

export const LocatorAPI = superclass =>
class extends superclass {
Expand All @@ -14,22 +22,19 @@ export const LocatorAPI = superclass =>
* @see https://www.ups.com/upsdeveloperkit?loc=en_US
*/
async getNearestAccessPoint(addressLine, city, postalCode, countryCode, options = {}) {
const data = this._buildNearestAccessPointPayload(
const url = `${this.baseUrl}locations/${this.version}/search/availabilities/${ACCESS_POINT_SEARCH}`;
const payload = this._buildNearestAccessPointPayload(
addressLine,
city,
postalCode,
countryCode,
options
);
const response = await this.post(this._getLocatorBaseUrl(), {
kwargs: { auth: "headers" },
mime: "application/xml",
data: data,
...options
const response = await this.post(url, {
...options,
dataJ: payload
});
const xml = await response.text();
const result = xmlToJson(xml);
return result;
return response;
}

_buildNearestAccessPointPayload(
Expand All @@ -39,42 +44,28 @@ export const LocatorAPI = superclass =>
countryCode,
{ consignee = null, locale = "en_US", metric = true, radius = 150 } = {}
) {
const xml =
getXMLHeader(this.username, this.password, this.license) +
`<?xml version="1.0"?>
<LocatorRequest>
<Request>
<RequestAction>Locator</RequestAction>
<RequestOption>64</RequestOption>
</Request>
<OriginAddress>
<AddressKeyFormat>
${consignee ? `<ConsigneeName>${consignee}</ConsigneeName>` : ""}
<AddressLine>${addressLine}</AddressLine>
<PoliticalDivision2>${city}</PoliticalDivision2>
<PostcodePrimaryLow>${postalCode}</PostcodePrimaryLow>
<CountryCode>${countryCode}</CountryCode>
</AddressKeyFormat>
</OriginAddress>
<Translate>
<Locale>${locale}</Locale>
</Translate>
<UnitOfMeasurement>
<Code>${metric ? "KM" : "MI"}</Code>
</UnitOfMeasurement>
<LocationSearchCriteria>
<SearchOption>
<OptionType>
<Code>01</Code>
</OptionType>
</SearchOption>
<MaximumListSize>1</MaximumListSize>
<SearchRadius>${radius}</SearchRadius>
<AccessPointSearch>
<AccessPointStatus>01</AccessPointStatus>
</AccessPointSearch>
</LocationSearchCriteria>
</LocatorRequest>`;
return xml;
const payload = {
LocatorRequest: {
Request: {
RequestAction: "Locator",
RequestOption: ACCESS_POINT_SEARCH
},
OriginAddress: {
AddressKeyFormat: {
ConsigneeName: consignee,
AddressLine: addressLine,
PoliticalDivision2: city,
PostcodePrimaryLow: postalCode,
CountryCode: countryCode
}
},
Translate: {
Locale: locale
},
UnitOfMeasurement: { Code: metric ? "KM" : "MI" },
SortCriteria: { SortType: CLOSEST_POINT_SEARCH }
}
};
return payload;
}
};
Loading

0 comments on commit 606f2f5

Please sign in to comment.