diff --git a/__tests__/shared/utils/challenge-listing/constants.test.js b/__tests__/shared/utils/challenge-listing/constants.test.js new file mode 100644 index 0000000000..ff07692216 --- /dev/null +++ b/__tests__/shared/utils/challenge-listing/constants.test.js @@ -0,0 +1,38 @@ +import { + getVisibleChallengeTypes, + sanitizeChallengeTypeFilter, +} from 'utils/challenge-listing/constants'; + +describe('challenge listing constants', () => { + test('returns only the supported challenge types once and in filter order', () => { + const challengeTypes = [ + { name: 'AI', abbreviation: 'AI' }, + { name: 'AI Engineering', abbreviation: 'AIENG' }, + { name: 'Marathon Match', abbreviation: 'MM' }, + { name: 'Challenge', abbreviation: 'CH' }, + { name: 'Marathon Match', abbreviation: 'MM' }, + { name: 'Task', abbreviation: 'TSK' }, + { name: 'First2Finish', abbreviation: 'F2F' }, + { name: 'type-1778748614529', abbreviation: 'type-1778748614529' }, + ]; + + expect(getVisibleChallengeTypes(challengeTypes)).toEqual([ + { name: 'Challenge', abbreviation: 'CH' }, + { name: 'First2Finish', abbreviation: 'F2F' }, + { name: 'Marathon Match', abbreviation: 'MM' }, + { name: 'Task', abbreviation: 'TSK' }, + ]); + }); + + test('removes hidden and duplicated selected challenge type filters', () => { + expect(sanitizeChallengeTypeFilter([ + 'AI', + 'CH', + 'F2F', + 'MM', + 'MM', + 'TSK', + 'type-1778748614529', + ])).toEqual(['CH', 'F2F', 'MM', 'TSK']); + }); +}); diff --git a/__tests__/shared/utils/mm-review-summations.test.js b/__tests__/shared/utils/mm-review-summations.test.js index e456ab9183..cd82a0c483 100644 --- a/__tests__/shared/utils/mm-review-summations.test.js +++ b/__tests__/shared/utils/mm-review-summations.test.js @@ -1,5 +1,8 @@ /* eslint-env jest */ -import { buildMmSubmissionData } from '../../../src/shared/utils/mm-review-summations'; +import { + buildMmSubmissionData, + buildStatisticsData, +} from '../../../src/shared/utils/mm-review-summations'; describe('buildMmSubmissionData', () => { it('keeps newer raw submissions that do not have review summations yet', () => { @@ -280,3 +283,112 @@ describe('buildMmSubmissionData', () => { ]); }); }); + +describe('buildStatisticsData', () => { + it('omits processing and failed scorer updates from dashboard graph data', () => { + const reviewSummations = [ + { + aggregateScore: 0, + id: 'summation-processing-zero', + isProvisional: true, + metadata: { + testStatus: 'IN PROGRESS', + testType: 'provisional', + }, + reviewedDate: '2026-05-29T01:00:00.000Z', + submissionId: 'submission-processing', + submitterHandle: 'alpha', + submitterId: '1001', + }, + { + aggregateScore: -1, + id: 'summation-failed', + isProvisional: true, + metadata: { + testStatus: 'FAILED', + testType: 'provisional', + }, + reviewedDate: '2026-05-29T01:05:00.000Z', + submissionId: 'submission-failed', + submitterHandle: 'alpha', + submitterId: '1001', + }, + { + aggregateScore: 0, + id: 'summation-success-zero', + isProvisional: true, + metadata: { + testStatus: 'SUCCESS', + testType: 'provisional', + }, + reviewedDate: '2026-05-29T01:10:00.000Z', + submissionId: 'submission-success-zero', + submitterHandle: 'beta', + submitterId: '1002', + }, + { + aggregateScore: 84.25, + id: 'summation-success', + isProvisional: true, + metadata: { + testStatus: 'SUCCESS', + testType: 'provisional', + }, + reviewedDate: '2026-05-29T01:15:00.000Z', + submissionId: 'submission-success', + submitterHandle: 'alpha', + submitterId: '1001', + }, + ]; + + const result = buildStatisticsData(reviewSummations); + + expect(result).toHaveLength(2); + expect(result).toEqual(expect.arrayContaining([ + expect.objectContaining({ + handle: 'alpha', + submissions: [ + expect.objectContaining({ + score: 84.25, + submissionId: 'submission-success', + }), + ], + }), + expect.objectContaining({ + handle: 'beta', + submissions: [ + expect.objectContaining({ + score: 0, + submissionId: 'submission-success-zero', + }), + ], + }), + ])); + }); + + it('keeps legacy non-negative summations when scorer status metadata is absent', () => { + const result = buildStatisticsData([ + { + aggregateScore: 72.5, + id: 'summation-legacy', + isProvisional: true, + reviewedDate: '2026-05-29T02:00:00.000Z', + submissionId: 'submission-legacy', + submitterHandle: 'gamma', + submitterId: '1003', + }, + ]); + + expect(result).toEqual([ + expect.objectContaining({ + handle: 'gamma', + submissions: [ + expect.objectContaining({ + score: 72.5, + submissionId: 'submission-legacy', + }), + ], + }), + ]); + }); +}); diff --git a/__tests__/shared/utils/terms.test.js b/__tests__/shared/utils/terms.test.js new file mode 100644 index 0000000000..ca7167f140 --- /dev/null +++ b/__tests__/shared/utils/terms.test.js @@ -0,0 +1,35 @@ +import { + getDocuSignTemplateIdForTerm, + isNdaTerm, +} from 'utils/terms'; + +describe('terms utils', () => { + const NEW_NDA_TEMPLATE_ID = '400b989d-1c75-4889-b6f6-421e1f924709'; + + test('detects NDA terms by title', () => { + expect(isNdaTerm({ title: 'Appirio NDA v2.0' })).toBe(true); + expect(isNdaTerm({ title: 'Competition Non-Disclosure Agreement' })).toBe(true); + expect(isNdaTerm({ title: 'Assignment Terms' })).toBe(false); + }); + + test('uses configured DocuSign template for NDA terms', () => { + expect(getDocuSignTemplateIdForTerm({ + docusignTemplateId: 'old-template-id', + title: 'Appirio NDA v2.0', + })).toBe(NEW_NDA_TEMPLATE_ID); + }); + + test('keeps terms-service template for non-NDA terms', () => { + expect(getDocuSignTemplateIdForTerm({ + docusignTemplateId: 'assignment-template-id', + title: 'Assignment Terms', + })).toBe('assignment-template-id'); + }); + + test('handles missing terms details', () => { + expect(isNdaTerm(null)).toBe(false); + expect(isNdaTerm()).toBe(false); + expect(getDocuSignTemplateIdForTerm(null)).toBe(undefined); + expect(getDocuSignTemplateIdForTerm()).toBe(undefined); + }); +}); diff --git a/config/backup-default.js b/config/backup-default.js index 4bacfa2b87..40c716e382 100644 --- a/config/backup-default.js +++ b/config/backup-default.js @@ -70,6 +70,12 @@ module.exports = { * agreement flow. */ MOCK_TERMS_SERVICE: false, + /* Optional DocuSign template override for NDA-like terms. When set, the + * terms modal requests this template for terms whose title includes NDA or + * Non-Disclosure, even if the Terms API still returns an older template id. + */ + NDA_DOCUSIGN_TEMPLATE_ID: '400b989d-1c75-4889-b6f6-421e1f924709', + /* Holds params to signup for different newsletters. */ NEWSLETTER_SIGNUP: { DEFAUL_LIST_ID: '28bfd3c062', diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index 88dd2a9a04..a6dac4008d 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -12,6 +12,7 @@ module.exports = { DISABLE_SERVICE_WORKER: 'DISABLE_SERVICE_WORKER', LOG_ENTRIES_TOKEN: 'LOG_ENTRIES_TOKEN', MOCK_TERMS_SERVICE: 'MOCK_TERMS_SERVICE', + NDA_DOCUSIGN_TEMPLATE_ID: 'NDA_DOCUSIGN_TEMPLATE_ID', NEWSLETTER_SIGNUP: { COGNITIVE: { diff --git a/config/default.js b/config/default.js index ef3558fdae..c8f23a3e43 100644 --- a/config/default.js +++ b/config/default.js @@ -72,6 +72,12 @@ module.exports = { * agreement flow. */ MOCK_TERMS_SERVICE: false, + /* Optional DocuSign template override for NDA-like terms. When set, the + * terms modal requests this template for terms whose title includes NDA or + * Non-Disclosure, even if the Terms API still returns an older template id. + */ + NDA_DOCUSIGN_TEMPLATE_ID: '400b989d-1c75-4889-b6f6-421e1f924709', + /* Holds params to signup for different newsletters. */ NEWSLETTER_SIGNUP: { DEFAUL_LIST_ID: '28bfd3c062', diff --git a/config/production.js b/config/production.js index 40d72858af..f805153f72 100644 --- a/config/production.js +++ b/config/production.js @@ -18,6 +18,7 @@ module.exports = { SECURE: true, }, LOG_ENTRIES_TOKEN: '', + NDA_DOCUSIGN_TEMPLATE_ID: '8b101e82-87c0-42c9-8440-d922749c4076', SERVER_API_KEY: 'aa9ccf36-3936-450c-9983-097ddba51bef', GOOGLE_ANALYTICS_ID: 'UA-6340959-1', URL: { diff --git a/config/qa.js b/config/qa.js index 7ac7b5291b..9625992fcd 100644 --- a/config/qa.js +++ b/config/qa.js @@ -1,5 +1,6 @@ module.exports = { SEGMENT_IO_API_KEY: 'QBtLgV8vCiuRX1lDikbMjcoe9aCHkF6n', + NDA_DOCUSIGN_TEMPLATE_ID: '', SERVER_API_KEY: '79b2d5eb-c1fd-42c4-9391-6b2c9780d591', API: { ENGAGEMENTS: 'https://api.topcoder-qa.com/v6/engagements/engagements', diff --git a/src/shared/components/Terms/TermDetails.jsx b/src/shared/components/Terms/TermDetails.jsx index b0ec297f8b..d220fd05e3 100644 --- a/src/shared/components/Terms/TermDetails.jsx +++ b/src/shared/components/Terms/TermDetails.jsx @@ -6,6 +6,7 @@ import React from 'react'; import PT from 'prop-types'; import LoadingIndicator from 'components/LoadingIndicator'; +import { getDocuSignTemplateIdForTerm } from 'utils/terms'; import './TermDetails.scss'; @@ -20,8 +21,9 @@ export default class TermDetails extends React.Component { componentWillMount() { const { details, getDocuSignUrl } = this.props; - if (details.agreeabilityType !== 'Electronically-agreeable' && details.docusignTemplateId) { - getDocuSignUrl(details.docusignTemplateId); + const docusignTemplateId = getDocuSignTemplateIdForTerm(details); + if (docusignTemplateId) { + getDocuSignUrl(docusignTemplateId); this.setState({ loadingFrame: true }); } } @@ -38,11 +40,14 @@ export default class TermDetails extends React.Component { loadingDocuSignUrl, } = this.props; const { loadingFrame } = this.state; + const docusignTemplateId = getDocuSignTemplateIdForTerm(details); + const isDocuSignTerm = Boolean(docusignTemplateId); return (
{ details.agreeabilityType === 'Electronically-agreeable' + && !isDocuSignTerm && (
} { - details.agreeabilityType !== 'Electronically-agreeable' && details.docusignTemplateId - && !loadingDocuSignUrl && docuSignUrl + isDocuSignTerm && !loadingDocuSignUrl && docuSignUrl && (
{ diff --git a/src/shared/components/Terms/index.jsx b/src/shared/components/Terms/index.jsx index 49ad395401..a77f2249c3 100644 --- a/src/shared/components/Terms/index.jsx +++ b/src/shared/components/Terms/index.jsx @@ -11,6 +11,7 @@ import PT from 'prop-types'; import { Modal, PrimaryButton, Button } from 'topcoder-react-ui-kit'; import LoadingIndicator from 'components/LoadingIndicator'; import FocusTrap from 'focus-trap-react'; +import { getDocuSignTemplateIdForTerm } from 'utils/terms'; import TermDetails from './TermDetails'; import style from './styles.scss'; @@ -139,6 +140,7 @@ export default class Terms extends React.Component { loadingDocuSignUrl, selectedTerm, viewOnly, checkingStatus, description, defaultTitle, } = this.props; + const isDocuSignTerm = Boolean(getDocuSignTemplateIdForTerm(details)); const handleHorizonalScroll = (e) => { const scrollElement = e.target; @@ -283,7 +285,8 @@ export default class Terms extends React.Component { !isLoadingTerms && !checkingStatus && selectedTerm && details && !viewOnly && loadingTermId !== _.toString(selectedTerm.id) - && details.agreeabilityType === 'Electronically-agreeable' ? ( + && details.agreeabilityType === 'Electronically-agreeable' + && !isDocuSignTerm ? (
{ selectedTerm.agreed diff --git a/src/shared/components/challenge-detail/Specification/SpecificationComponent/styles.scss b/src/shared/components/challenge-detail/Specification/SpecificationComponent/styles.scss index a1be329593..273a89b966 100644 --- a/src/shared/components/challenge-detail/Specification/SpecificationComponent/styles.scss +++ b/src/shared/components/challenge-detail/Specification/SpecificationComponent/styles.scss @@ -2,6 +2,11 @@ .container { :global { + ul, + ol { + padding-left: 20px; + } + table { display: block; width: 100%; diff --git a/src/shared/containers/challenge-listing/FilterPanel.jsx b/src/shared/containers/challenge-listing/FilterPanel.jsx index 367e70f732..b6b3e2103b 100644 --- a/src/shared/containers/challenge-listing/FilterPanel.jsx +++ b/src/shared/containers/challenge-listing/FilterPanel.jsx @@ -17,7 +17,10 @@ import { connect } from 'react-redux'; import qs from 'qs'; import _ from 'lodash'; import { createStaticRanges } from 'utils/challenge-listing/date-range'; -import { EXCLUDED_CHALLENGE_TYPE_NAMES } from 'utils/challenge-listing/constants'; +import { + getVisibleChallengeTypes, + sanitizeChallengeTypeFilter, +} from 'utils/challenge-listing/constants'; const MIN = 60 * 1000; @@ -74,7 +77,9 @@ export class Container extends React.Component { query.customDate = customDate; } - if (query.types && query.types.length) { + if (query.types && sanitizeChallengeTypeFilter( + Array.isArray(query.types) ? query.types : [query.types], + ).length) { this.initialDefaultChallengeTypes = true; } @@ -105,8 +110,7 @@ export class Container extends React.Component { }); this.initialDefaultChallengeTypes = true; } else if (validTypes.length && currentTypes.length) { - const validAbbreviations = validTypes.map(item => item.abbreviation); - const sanitizedTypes = currentTypes.filter(type => validAbbreviations.includes(type)); + const sanitizedTypes = sanitizeChallengeTypeFilter(currentTypes); if (sanitizedTypes.length !== currentTypes.length) { if (!sanitizedTypes.length) { this.initialDefaultChallengeTypes = false; @@ -235,16 +239,11 @@ function mapDispatchToProps(dispatch) { function mapStateToProps(state, ownProps) { const cl = state.challengeListing; const tc = state.tcCommunities; - const filteredChallengeTypes = cl.challengeTypes - .filter(type => !EXCLUDED_CHALLENGE_TYPE_NAMES.includes(type.name)); - const excludedTypeAbbreviations = cl.challengeTypes - .filter(type => EXCLUDED_CHALLENGE_TYPE_NAMES.includes(type.name)) - .map(type => type.abbreviation); + const filteredChallengeTypes = getVisibleChallengeTypes(cl.challengeTypes); let filterState = cl.filter; const existingTypes = Array.isArray(cl.filter.types) ? cl.filter.types : []; - if (excludedTypeAbbreviations.length && existingTypes.length) { - const sanitizedTypes = existingTypes - .filter(type => !excludedTypeAbbreviations.includes(type)); + if (existingTypes.length) { + const sanitizedTypes = sanitizeChallengeTypeFilter(existingTypes); if (sanitizedTypes.length !== existingTypes.length) { filterState = { ...cl.filter, diff --git a/src/shared/containers/terms-detail/index.jsx b/src/shared/containers/terms-detail/index.jsx index 66755806ec..9aa78f4dae 100644 --- a/src/shared/containers/terms-detail/index.jsx +++ b/src/shared/containers/terms-detail/index.jsx @@ -15,6 +15,7 @@ import MetaTags from 'components/MetaTags'; import { Modal, PrimaryButton } from 'topcoder-react-ui-kit'; import SwitchWithLabel from 'components/SwitchWithLabel'; import { themr } from 'react-css-super-themr'; +import { getDocuSignTemplateIdForTerm } from 'utils/terms'; import styles from './styles.scss'; const ACCEPTANCE_LABEL = 'I understand and agree'; @@ -87,6 +88,7 @@ class TermsDetailPageContainer extends React.Component { theme, } = this.props; const { termsAccepted, showModal } = this.state; + const isDocuSignTerm = Boolean(getDocuSignTemplateIdForTerm(details)); return (
@@ -131,7 +133,7 @@ class TermsDetailPageContainer extends React.Component { { agreeingTerm !== termId && details && !details.agreed && agreeTermFailure === undefined - && details.agreeabilityType !== 'DocuSignable' + && !isDocuSignTerm ? (
EXCLUDED_CHALLENGE_TYPE_NAMES.includes(type.name)) - .map(type => type.abbreviation); - if (excludedTypeAbbreviations.length && Array.isArray(basePayload.types)) { - sanitizedPayload.types = basePayload.types - .filter(type => !excludedTypeAbbreviations.includes(type)); + if (Array.isArray(basePayload.types)) { + sanitizedPayload.types = sanitizeChallengeTypeFilter(basePayload.types); } const filter = _.pickBy(_.pick( sanitizedPayload, diff --git a/src/shared/utils/challenge-listing/constants.js b/src/shared/utils/challenge-listing/constants.js index fbddb9fdca..520cb09147 100644 --- a/src/shared/utils/challenge-listing/constants.js +++ b/src/shared/utils/challenge-listing/constants.js @@ -1,4 +1,71 @@ -const EXCLUDED_CHALLENGE_TYPE_NAMES = ['Topgear Task']; +const VISIBLE_CHALLENGE_TYPES = [ + { + name: 'Challenge', + abbreviation: 'CH', + }, + { + name: 'First2Finish', + abbreviation: 'F2F', + }, + { + name: 'Marathon Match', + abbreviation: 'MM', + }, + { + name: 'Task', + abbreviation: 'TSK', + }, +]; -export { EXCLUDED_CHALLENGE_TYPE_NAMES }; -export default EXCLUDED_CHALLENGE_TYPE_NAMES; +const VISIBLE_CHALLENGE_TYPE_ABBREVIATIONS = VISIBLE_CHALLENGE_TYPES + .map(type => type.abbreviation); + +/** + * Returns the challenge types that should be displayed in listing filters. + * + * @param {Array} challengeTypes Challenge type records loaded from the + * challenge API. + * @return {Array} One API challenge type record per visible type, in + * the order used by the filter panel. + */ +function getVisibleChallengeTypes(challengeTypes = []) { + if (!Array.isArray(challengeTypes)) { + return []; + } + + return VISIBLE_CHALLENGE_TYPES + .map(visibleType => ( + challengeTypes.find(type => ( + type.name === visibleType.name + && type.abbreviation === visibleType.abbreviation + )) + || challengeTypes.find(type => type.name === visibleType.name) + )) + .filter(Boolean); +} + +/** + * Removes hidden and duplicated challenge type filter values. + * + * @param {Array} types Selected challenge type abbreviations. + * @return {Array} Selected abbreviations limited to the visible filter + * types. + */ +function sanitizeChallengeTypeFilter(types = []) { + if (!Array.isArray(types)) { + return types; + } + + return types.filter((type, index) => ( + VISIBLE_CHALLENGE_TYPE_ABBREVIATIONS.includes(type) + && types.indexOf(type) === index + )); +} + +export { + VISIBLE_CHALLENGE_TYPES, + VISIBLE_CHALLENGE_TYPE_ABBREVIATIONS, + getVisibleChallengeTypes, + sanitizeChallengeTypeFilter, +}; +export default VISIBLE_CHALLENGE_TYPES; diff --git a/src/shared/utils/mm-review-summations.js b/src/shared/utils/mm-review-summations.js index d89b3fc863..5d5ba721f7 100644 --- a/src/shared/utils/mm-review-summations.js +++ b/src/shared/utils/mm-review-summations.js @@ -56,6 +56,56 @@ function getSummationScoreClassification(summation) { }; } +/** + * Normalizes scorer status values for comparison. + * Used for Marathon Match dashboard statistics, where Review API exposes + * scorer progress in review summation metadata. + * + * @param {*} status Raw scorer status value. + * @returns {String|null} Normalized status or null when absent. + */ +function normalizeScoringStatus(status) { + const normalizedStatus = _.toLower(_.toString(status || '').trim()) + .replace(/[\s_-]+/g, ''); + return normalizedStatus || null; +} + +/** + * Returns whether a review summation represents completed successful scoring. + * Summations with explicit in-progress or failed scorer metadata are kept out + * of the public Marathon Match dashboard graph. Legacy summations without + * scorer metadata are still included when they have a non-negative score. + * + * @param {Object} summation Review summation returned by Review API. + * @param {Number} score Normalized aggregate score for the summation. + * @returns {Boolean} True when the summation can be graphed. + */ +function hasCompletedSuccessfulScoring(summation, score) { + const metadata = _.isObject(_.get(summation, 'metadata')) + ? _.get(summation, 'metadata') + : {}; + const scoringStatus = normalizeScoringStatus(_.get(metadata, 'testStatus')); + const successStatuses = [ + 'complete', + 'completed', + 'pass', + 'passed', + 'success', + 'succeeded', + ]; + + if (scoringStatus) { + return successStatuses.includes(scoringStatus); + } + + const progress = Number(_.get(metadata, 'testProgress')); + if (Number.isFinite(progress) && progress < 1) { + return false; + } + + return score >= 0; +} + function toTimestampValue(value) { if (!value) { return 0; @@ -825,35 +875,48 @@ export function buildMmSubmissionData(reviewSummations = [], rawSubmissions = [] }); } +/** + * Builds Marathon Match dashboard graph data from Review API summations. + * Processing and failed scorer updates are omitted until a successful completed + * summation is available for the submission. + * + * @param {Array} reviewSummations Review summations returned by Review API. + * @returns {Array} Member-grouped submission score points for the dashboard. + */ export function buildStatisticsData(reviewSummations = []) { if (!Array.isArray(reviewSummations) || !reviewSummations.length) { return []; } - const includeOnlyProvisional = reviewSummations.some((summation) => { + const graphableSummations = []; + reviewSummations.forEach((summation, index) => { if (!summation) { - return false; + return; } const score = normalizeScoreValue(_.get(summation, 'aggregateScore')); if (_.isNil(score)) { - return false; + return; + } + if (!hasCompletedSuccessfulScoring(summation, score)) { + return; } - return getSummationScoreClassification(summation).isProvisional; + + graphableSummations.push({ index, score, summation }); }); - const grouped = new Map(); + if (!graphableSummations.length) { + return []; + } - reviewSummations.forEach((summation, index) => { - if (!summation) { - return; - } + const includeOnlyProvisional = graphableSummations.some( + ({ summation }) => getSummationScoreClassification(summation).isProvisional, + ); + + const grouped = new Map(); + graphableSummations.forEach(({ index, score, summation }) => { const timestamp = getSummationTimestamp(summation); const timestampValue = toTimestampValue(timestamp); - const score = normalizeScoreValue(_.get(summation, 'aggregateScore')); - if (_.isNil(score)) { - return; - } const scoreType = getSummationScoreClassification(summation); if (includeOnlyProvisional) { if (!scoreType.isProvisional) { diff --git a/src/shared/utils/terms.js b/src/shared/utils/terms.js new file mode 100644 index 0000000000..192e6d6ff0 --- /dev/null +++ b/src/shared/utils/terms.js @@ -0,0 +1,29 @@ +import { config } from 'topcoder-react-utils'; + +const NDA_TITLE_PATTERN = /\bnda\b|non[-\s]?disclosure/i; + +/** + * Checks whether a terms record represents an NDA-style agreement. + * + * @param {Object|null} term terms-service record or details payload. + * @returns {Boolean} true when the term title is NDA/non-disclosure related. + */ +export function isNdaTerm(term = {}) { + return NDA_TITLE_PATTERN.test((term && term.title) || ''); +} + +/** + * Resolves the DocuSign template id to use for a terms-service record. + * + * @param {Object|null} term terms-service record or details payload. + * @returns {String|Number|undefined} configured NDA template id for NDA terms, + * or the template id returned by terms-service for all other terms. + */ +export function getDocuSignTemplateIdForTerm(term = {}) { + const configuredNdaTemplateId = config.NDA_DOCUSIGN_TEMPLATE_ID; + if (configuredNdaTemplateId && isNdaTerm(term)) { + return configuredNdaTemplateId; + } + + return term ? term.docusignTemplateId : undefined; +}