diff --git a/.circleci/config.yml b/.circleci/config.yml index 154fcd7..b59e2bf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,4 @@ -version: 2 +version: 2.1 machine: services: - docker @@ -7,6 +7,9 @@ jobs: build: docker: - image: circleci/node:8@sha256:ff2c2a3fd5105396697de4c20139435fe9f47d62716fa241d225122deb711d50 + auth: + username: $DOCKERHUB_USERNAME + password: $DOCKERHUB_PASSWORD environment: - NPM_CONFIG_LOGLEVEL: warn working_directory: ~/repo @@ -28,3 +31,10 @@ jobs: - deploy: name: Release Code command: npm run release + +workflows: + build: + jobs: + - build: + context: + - docker-hub-creds diff --git a/package-lock.json b/package-lock.json index accad12..0f85a92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -470,12 +470,19 @@ "dev": true }, "axios": { - "version": "0.18.0", - "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", - "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", "requires": { - "follow-redirects": "^1.3.0", - "is-buffer": "^1.1.5" + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + } } }, "babel-code-frame": { @@ -2335,9 +2342,9 @@ } }, "follow-redirects": { - "version": "1.5.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.9.tgz", - "integrity": "sha512-Bh65EZI/RU8nx0wbYF9shkFZlqLP+6WT/5FnA3cE/djNSuKNHJEinGGZgu/cQEkeeb2GdFOgenAmn8qaqYke2w==", + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", "requires": { "debug": "=3.1.0" }, @@ -2868,7 +2875,8 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true }, "is-builtin-module": { "version": "1.0.0", diff --git a/package.json b/package.json index 3f7c24b..39ad90a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "check-coverage": true }, "dependencies": { - "axios": "^0.18.0", + "axios": "^0.19.0", "base-64": "^0.1.0", "lodash": "^4.17.11", "moment": "^2.24.0" diff --git a/src/examples/completedSale.ts b/src/examples/completedSale.ts new file mode 100644 index 0000000..38152a1 --- /dev/null +++ b/src/examples/completedSale.ts @@ -0,0 +1,20 @@ +import completedSale from '../statementCreators/completedSale'; + +const statement = completedSale({ + actionDate: new Date(), + activityUrl: 'https://demo.example.org/courses/demo-course', + siteUrl: 'https://demo.example.org', + siteName: 'Demo Example Site', + platformUrl: 'https://example.org', + platformName: 'Example Platform', + userId: '123', + userIdProviderUrl: 'https://demo.example.org', + userEmail: 'demo@example.org', + userDisplayName: 'Demo User', + isWon: true, + closedReason: 'Too expensive', + accountDisplayName: 'Example Account', + accountUrl: 'https://demo.example.org', +}); + +export default statement; diff --git a/src/examples/generatedLead.ts b/src/examples/generatedLead.ts new file mode 100644 index 0000000..63c418d --- /dev/null +++ b/src/examples/generatedLead.ts @@ -0,0 +1,16 @@ +import generatedLead from '../statementCreators/generatedLead'; + +const statement = generatedLead({ + actionDate: new Date(), + activityUrl: 'https://demo.example.org/courses/demo-course', + siteUrl: 'https://demo.example.org', + siteName: 'Demo Example Site', + platformUrl: 'https://example.org', + platformName: 'Example Platform', + userId: '123', + userIdProviderUrl: 'https://demo.example.org', + userEmail: 'demo@example.org', + userDisplayName: 'Demo User', +}); + +export default statement; diff --git a/src/extensions/extendWithCostCenter.ts b/src/extensions/extendWithCostCenter.ts new file mode 100644 index 0000000..17a84c0 --- /dev/null +++ b/src/extensions/extendWithCostCenter.ts @@ -0,0 +1,10 @@ +import { baseUrl } from '../statementConstants/extensions'; + +export function extendWithCostCenter(costCenterId?: string) { + if (costCenterId === undefined) { + return {}; + } + return { + [`${baseUrl}/cost-center-id`]: costCenterId, + }; +} diff --git a/src/statementConstants/activityTypes.ts b/src/statementConstants/activityTypes.ts index cc6de5f..aeefe12 100644 --- a/src/statementConstants/activityTypes.ts +++ b/src/statementConstants/activityTypes.ts @@ -89,6 +89,11 @@ export const image = 'http://activitystrea.ms/schema/1.0/image'; */ export const issue = 'http://activitystrea.ms/schema/1.0/issue'; +/** + * Represents a person or business who may eventually become a client + */ +export const salesLead = `${customBaseUrl}/sales-lead`; + /** * Means of expressing a link to another resource within, or external to, an activity. * Not synonymous with launching another resource. @@ -172,6 +177,11 @@ export const resourceStructure = `${customBaseUrl}/resource-structure`; */ export const resourceStructureNode = `${customBaseUrl}/resource-structure-node`; +/** + * Represents a sales opportunity. + */ +export const salesOpportunity = 'http://id.tincanapi.com/activitytype/sales-opportunity'; + /** * Represents a feature to enable admins to manage access on a per-user or per-group basis. */ diff --git a/src/statementConstants/extensions.ts b/src/statementConstants/extensions.ts index 03d4e53..2ab6607 100644 --- a/src/statementConstants/extensions.ts +++ b/src/statementConstants/extensions.ts @@ -1,4 +1,4 @@ -const baseUrl = 'http://learninglocker.net/xapi/extensions'; +export const baseUrl = 'http://learninglocker.net/xapi/extensions'; export const dueDateExtension = `${baseUrl}/due-date`; export const booleanResponseExtension = `${baseUrl}/boolean-response`; diff --git a/src/statementConstants/verbs.ts b/src/statementConstants/verbs.ts index 1f1d7c7..7e81278 100644 --- a/src/statementConstants/verbs.ts +++ b/src/statementConstants/verbs.ts @@ -12,11 +12,19 @@ export const createVerb = (id: string, display: string): Verb => { export const accessed = createVerb('http://activitystrea.ms/schema/1.0/access', 'accessed'); export const activated = createVerb('https://w3id.org/xapi/dod-isd/verbs/activated', 'activated'); export const answered = createVerb('http://adlnet.gov/expapi/verbs/answered', 'answered'); -export const assigned = createVerb('https://w3id.org/xapi/acrossx/verbs/was-assigned', 'assigned'); +export const assigned = createVerb( + 'https://w3id.org/xapi/acrossx/verbs/was-assigned', + 'was assigned to', +); +export const attempted = createVerb('http://adlnet.gov/expapi/verbs/attempted', 'attempted'); export const attended = createVerb('http://activitystrea.ms/schema/1.0/attend', 'attended'); export const bookmarked = createVerb('http://id.tincanapi.com/verb/bookmarked', 'bookmarked'); export const called = createVerb('http://id.tincanapi.com/verb/called', 'called'); export const canceled = createVerb('https://w3id.org/xapi/dod-isd/verbs/canceled', 'canceled'); +export const closedSale = createVerb( + 'http://id.tincanapi.com/verb/closed-sale', + 'closed a sale with', +); export const commentedOn = createVerb('http://adlnet.gov/expapi/verbs/commented', 'commented on'); export const completed = createVerb('http://adlnet.gov/expapi/verbs/completed', 'completed'); export const created = createVerb('http://activitystrea.ms/schema/1.0/create', 'created'); @@ -28,6 +36,10 @@ export const evaluated = createVerb('http://www.tincanapi.co.uk/verbs/evaluated' export const exited = createVerb('http://adlnet.gov/expapi/verbs/exited', 'exited'); export const filled = createVerb('https://w3id.org/xapi/dod-isd/verbs/filled-out', 'filled'); export const followed = createVerb('https://w3id.org/xapi/dod-isd/verbs/followed', 'followed'); +export const generated = createVerb( + 'https://w3id.org/xapi/dod-isd/verbs/generated', + 'generated a lead with', +); export const joined = createVerb('http://activitystrea.ms/schema/1.0/join', 'joined'); export const launched = createVerb('http://adlnet.gov/expapi/verbs/launched', 'launched'); export const liked = createVerb('https://w3id.org/xapi/acrossx/verbs/liked', 'liked'); diff --git a/src/statementCreators/actionOnSiteActivity.ts b/src/statementCreators/actionOnSiteActivity.ts index 6ccb9c9..3e4249a 100644 --- a/src/statementCreators/actionOnSiteActivity.ts +++ b/src/statementCreators/actionOnSiteActivity.ts @@ -20,6 +20,12 @@ export interface SiteActivityAction extends UserSiteActivityAction { /** Determines how long the activity took. */ readonly duration?: Duration; + + /** Score achieved in the activity as a percentage (decimal between -1 and 1). */ + readonly scaledScore?: number; + + /** Raw score achieved in the activity. */ + readonly rawScore?: number; } /** @@ -48,6 +54,12 @@ export default function actionOnSiteActivity(action: SiteActivityAction): Statem duration: action.duration === undefined ? undefined : ( action.duration.toISOString() ), + ...pickFilled({ + score: pickDefined({ + scaled: action.scaledScore, + raw: action.rawScore, + }), + }), }), }), context: { diff --git a/src/statementCreators/completedSale.ts b/src/statementCreators/completedSale.ts new file mode 100644 index 0000000..da97e03 --- /dev/null +++ b/src/statementCreators/completedSale.ts @@ -0,0 +1,82 @@ +import UserSiteAction from '../actionUtils/UserSiteAction'; +import { organization, salesOpportunity, site, source } from '../statementConstants/activityTypes'; +import { closedSale } from '../statementConstants/verbs'; +import createActivity from '../statementUtils/createActivity'; +import createAgent from '../statementUtils/createAgent'; +import createTimestamp from '../statementUtils/createTimestamp'; +import { Extensions, Statement } from '../statementUtils/types'; + +export interface CompletedSaleAction extends UserSiteAction { + /** The URL where the activity can be accessed. */ + readonly activityUrl: string; + + /** The human readable name for the activity. */ + readonly activityName?: string; + + /** Additional properties of the activity. */ + readonly activityExtensions?: Extensions; + + /** Determines if the sale was a success or failure. */ + readonly isWon?: boolean; + + /** The reason for which a sale or opportunity is closed, usually when lost */ + readonly closedReason?: string; + + /** The URL or identifier of the account or organization linked to the sale or opportunity. */ + readonly accountUrl: string; + + /** The name of the account or organization linked to the sale or opportunity. */ + readonly accountDisplayName?: string; +} + +/** + * Creates an xAPI Statement to represent a user completing a face-to-face meeting. + */ +export default function completedSale(action: CompletedSaleAction): Statement { + return { + timestamp: createTimestamp(action.actionDate), + actor: createAgent({ + displayName: action.userDisplayName, + id: action.userId, + idProviderUrl: action.userIdProviderUrl, + email: action.userEmail, + }), + verb: closedSale, + object: createActivity({ + type: salesOpportunity, + url: action.activityUrl, + name: action.activityName, + extensions: action.activityExtensions, + }), + context: { + platform: action.platformName, + language: 'en', + extensions: action.contextExtensions, + contextActivities: { + grouping: [ + createActivity({ + type: site, + url: action.siteUrl, + name: action.siteName, + }), + ], + parent: [ + createActivity({ + type: organization, + url: action.accountUrl, + name: action.accountDisplayName, + }), + ], + category: [createActivity({ + type: source, + url: action.platformUrl, + name: action.platformName, + })], + }, + }, + result: { + success: action.isWon, + response: action.closedReason, + }, + }; +} diff --git a/src/statementCreators/generatedLead.ts b/src/statementCreators/generatedLead.ts new file mode 100644 index 0000000..5b3f80e --- /dev/null +++ b/src/statementCreators/generatedLead.ts @@ -0,0 +1,57 @@ +import UserSiteAction from '../actionUtils/UserSiteAction'; +import { salesLead, site, source } from '../statementConstants/activityTypes'; +import { generated } from '../statementConstants/verbs'; +import createActivity from '../statementUtils/createActivity'; +import createAgent from '../statementUtils/createAgent'; +import createTimestamp from '../statementUtils/createTimestamp'; +import { Extensions, Statement } from '../statementUtils/types'; + +export interface GeneratedLeadAction extends UserSiteAction { + /** The URL where the activity can be accessed. */ + readonly activityUrl: string; + + /** The human readable name for the activity. */ + readonly activityName?: string; + + /** Additional properties of the activity. */ + readonly activityExtensions?: Extensions; +} + +/** + * Creates an xAPI Statement to represent a user completing a face-to-face meeting. + */ +export default function generatedLead(action: GeneratedLeadAction): Statement { + return { + timestamp: createTimestamp(action.actionDate), + actor: createAgent({ + displayName: action.userDisplayName, + id: action.userId, + idProviderUrl: action.userIdProviderUrl, + email: action.userEmail, + }), + verb: generated, + object: createActivity({ + type: salesLead, + url: action.activityUrl, + name: action.activityName, + extensions: action.activityExtensions, + }), + context: { + platform: action.platformName, + language: 'en', + extensions: action.contextExtensions, + contextActivities: { + grouping: [createActivity({ + type: site, + url: action.siteUrl, + name: action.siteName, + })], + category: [createActivity({ + type: source, + url: action.platformUrl, + name: action.platformName, + })], + }, + }, + }; +}