diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index d85a680f..f47bed15 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -28,7 +28,7 @@ jobs: deploy-server: name: 🚀 Deploy server needs: test-client - environment: + environment: name: production url: https://applicants-api.nycplanningdigital.com runs-on: ubuntu-latest @@ -36,6 +36,8 @@ jobs: - uses: actions/checkout@v4 with: sparse-checkout: server + - name: 'Install Heroku CLI' + run: curl https://cli-assets.heroku.com/install.sh | sh - uses: akhileshns/heroku-deploy@v3.13.15 name: Deploy server to Heroku with: @@ -75,6 +77,7 @@ jobs: HD_PAYMENT_IP_RANGE: ${{ secrets.PAYMENT_IP_RANGE }} HD_PAYMENT_STEP1_URL: ${{ secrets.PAYMENT_STEP1_URL }} HD_RER_FILETYPE_UUID: ${{ secrets.RER_FILETYPE_UUID }} + HD_LETTER_FILETYPE_UUID: ${{ secrets.LETTER_FILETYPE_UUID }} HD_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} HD_SHAREPOINT_CLIENT_ID: ${{ secrets.SHAREPOINT_CLIENT_ID }} HD_SHAREPOINT_CLIENT_SECRET: ${{ secrets.SHAREPOINT_CLIENT_SECRET }} @@ -107,7 +110,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 12.x - - name: Install application dependencies + - name: Install application dependencies working-directory: client run: yarn install --immutable --immutable-cache --check-cache - name: Build client diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index b6c79aad..59c6860d 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -36,6 +36,8 @@ jobs: - uses: actions/checkout@v4 with: sparse-checkout: server + - name: 'Install Heroku CLI' + run: curl https://cli-assets.heroku.com/install.sh | sh - uses: akhileshns/heroku-deploy@v3.13.15 name: Deploy server to Heroku with: @@ -72,6 +74,7 @@ jobs: HD_PAYMENT_IP_RANGE: ${{ secrets.PAYMENT_IP_RANGE }} HD_PAYMENT_STEP1_URL: ${{ secrets.PAYMENT_STEP1_URL }} HD_RER_FILETYPE_UUID: ${{ secrets.RER_FILETYPE_UUID }} + HD_LETTER_FILETYPE_UUID: ${{ secrets.LETTER_FILETYPE_UUID }} HD_SHAREPOINT_CLIENT_ID: ${{ secrets.SHAREPOINT_CLIENT_ID }} HD_SHAREPOINT_CLIENT_SECRET: ${{ secrets.SHAREPOINT_CLIENT_SECRET }} HD_SHAREPOINT_CRM_SITE: ${{ secrets.SHAREPOINT_CRM_SITE }} @@ -102,7 +105,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 12.x - - name: Install application dependencies + - name: Install application dependencies working-directory: client run: yarn install --immutable --immutable-cache --check-cache - name: Build client @@ -119,4 +122,3 @@ jobs: --site ${{secrets.NETLIFY_SITE_ID}} \ --auth ${{secrets.NETLIFY_AUTH_TOKEN}} \ --message "${{ github.event.head_commit.message }}" - diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index b2326ace..7a2c58ba 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -40,6 +40,8 @@ jobs: with: ref: 'main' sparse-checkout: server + - name: 'Install Heroku CLI' + run: curl https://cli-assets.heroku.com/install.sh | sh - uses: akhileshns/heroku-deploy@v3.13.15 name: Deploy server to Heroku with: @@ -63,6 +65,7 @@ jobs: HD_NYCID_TOKEN_SECRET: ${{ secrets.NYCID_TOKEN_SECRET }} HD_PAPERTRAIL_API_TOKEN: ${{ secrets.PAPERTRAIL_API_TOKEN }} HD_RER_FILETYPE_UUID: ${{ secrets.RER_FILETYPE_UUID }} + HD_LETTER_FILETYPE_UUID: ${{ secrets.LETTER_FILETYPE_UUID }} HD_SHAREPOINT_CLIENT_ID: ${{ secrets.SHAREPOINT_CLIENT_ID }} HD_SHAREPOINT_CLIENT_SECRET: ${{ secrets.SHAREPOINT_CLIENT_SECRET }} HD_SHAREPOINT_CRM_SITE: ${{ secrets.SHAREPOINT_CRM_SITE }} @@ -94,7 +97,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 12.x - - name: Install application dependencies + - name: Install application dependencies working-directory: client run: yarn install - name: Build client diff --git a/client/app/components/packages/pas-form/attached-documents.hbs b/client/app/components/packages/pas-form/attached-documents.hbs index ab10012c..4c3020d7 100644 --- a/client/app/components/packages/pas-form/attached-documents.hbs +++ b/client/app/components/packages/pas-form/attached-documents.hbs @@ -7,7 +7,6 @@ @attribute='documents' @validation={{@form.errors.documents.validation}} /> - + + + + diff --git a/client/app/components/projects/new.js b/client/app/components/projects/new.js index 5ab960b9..032b8c91 100644 --- a/client/app/components/projects/new.js +++ b/client/app/components/projects/new.js @@ -1,15 +1,25 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import SubmittableProjectsNewForm from '../../validations/submittable-projects-new-form'; import { optionset } from '../../helpers/optionset'; import config from '../../config/environment'; +import validateFileUpload from '../../validators/validate-file-presence'; export default class ProjectsNewFormComponent extends Component { validations = { SubmittableProjectsNewForm, }; + @tracked + submissionError = false; + + @tracked + isSubmitting = false; + + requestCounter = 0; + @service router; @@ -26,6 +36,13 @@ export default class ProjectsNewFormComponent extends Component { @action async submitProject() { + this.submissionError = false; + if (this.isSubmitting) return; + this.isSubmitting = true; + const requestStartTime = Date.now(); + this.requestCounter++; + console.log(`LOGGER: [Total Requests Made in the client controller] ${this.requestCounter}`); + const primaryContactInput = { first: this.args.package.primaryContactFirstName, last: this.args.package.primaryContactLastName, @@ -42,6 +59,17 @@ export default class ProjectsNewFormComponent extends Component { const contactInputs = [primaryContactInput, applicantInput]; + const validationResult = validateFileUpload( + { + message: 'Please upload at least one file before submitting.', + }, + )('documents', this.args.package.documents); + + if (validationResult !== true) { + this.errorMessage = validationResult; + return; + } + try { const contactPromises = contactInputs.map((contact) => this.store.queryRecord('contact', { email: contact.email, @@ -96,17 +124,31 @@ export default class ProjectsNewFormComponent extends Component { dcpApplicanttype: this.args.package.applicantType.code, dcpProjectbrief: this.args.package.projectBrief, _dcpApplicantadministratorCustomerValue: - verifiedPrimaryContact.id, + verifiedPrimaryContact.id, _dcpApplicantCustomerValue: verifiedApplicant.id, }, }, }), }); const { data: project } = await response.json(); - this.router.transitionTo('project', project.id); + const requestEndTime = Date.now(); + console.debug(`LOGGER: POST request in the client controller to took ${requestEndTime - requestStartTime} ms`); + console.debug('response in client controller: ', response, 'response status: ', response.status); + + const artifactsId = project.attributes['dcp-artifactsid']; + if (artifactsId === undefined) { + throw new Error('failed to create project with artifact'); + } + + await this.args.package.saveAttachedFiles(artifactsId); + + this.router.transitionTo('projects'); } catch { + this.submissionError = true; /* eslint-disable-next-line no-console */ console.error('Error while creating project'); + } finally { + this.isSubmitting = false; } } } diff --git a/client/app/components/projects/projects-new-attached-documents.hbs b/client/app/components/projects/projects-new-attached-documents.hbs new file mode 100644 index 00000000..60642d84 --- /dev/null +++ b/client/app/components/projects/projects-new-attached-documents.hbs @@ -0,0 +1,23 @@ +{{#let @form as |form|}} +<@form.Section @title='Attached Documents'> +

+ Please attach the required items listed on the + + Informational Interest Meeting Checklist +  in at least one PDF document. The maximum + size for a document is 50MB. +

+ + + + + + +{{/let}} diff --git a/client/app/components/projects/projects-new-attachments.hbs b/client/app/components/projects/projects-new-attachments.hbs new file mode 100644 index 00000000..f771f631 --- /dev/null +++ b/client/app/components/projects/projects-new-attachments.hbs @@ -0,0 +1,133 @@ +
+
+ + Attachments + + + +
    + {{#each @fileManager.existingFiles as |file idx|}} +
  • +
    + +
    + + {{file.timeCreated}} + +
    +
    + +
    +
    +
  • + {{/each}} +
+ + {{#if (or @fileManager.filesToDelete @fileManager.filesToUpload.files)}} + {{#if @fileManager.existingFiles}} +
+ {{/if}} + +
+ To be + {{if @fileManager.filesToUpload.files 'uploaded'}} + {{if (and @fileManager.filesToDelete @fileManager.filesToUpload.files) '/'}} + {{if @fileManager.filesToDelete 'deleted'}} + when you save the project: +
+ {{/if}} + +
    + {{#each @fileManager.filesToDelete as |file idx|}} +
  • +
    +
    + + {{file.name}} + +
    +
    + + TO BE DELETED + +
    +
    + +
    +
    +
  • + {{/each}} +
+ +
    + {{#each @fileManager.filesToUpload.files as |file idx|}} +
  • +
    +
    + + {{file.name}} + +
    +
    + + TO BE ADDED + +
    +
    + +
    +
    +
  • + {{/each}} +
+ + + + Choose Files + + +

+ The size limit for each file is 50 MB. You can upload up to 1 GB of files. +

+
+
diff --git a/client/app/components/projects/projects-new-attachments.js b/client/app/components/projects/projects-new-attachments.js new file mode 100644 index 00000000..5e81ff89 --- /dev/null +++ b/client/app/components/projects/projects-new-attachments.js @@ -0,0 +1,63 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import validateFileUpload from '../../validators/validate-file-presence'; + +/** + * This component wires a fileManager to the attachments UI. + * @param {Artifact Model} artifact + */ +export default class ProjectsNewAttachmentsComponent extends Component { + get fileManager() { + // should be an instance of FileManager + return this.args.fileManager; + } + + @action + markFileForDeletion(file) { + this.fileManager.markFileForDeletion(file); + + this.args.package.documents = this.fileManager.existingFiles; + } + + @action + unmarkFileForDeletion(file) { + this.fileManager.unMarkFileForDeletion(file); + } + + // This action doesn't perform any file selection. + // That part is automatically handled by the + // ember-file-upload addon. + // Here we manually increment the number of files to + // upload to update the fileManager isDirty state. + @action + trackFileForUpload() { + this.fileManager.trackFileForUpload(); + this.args.package.documents = [ + ...this.args.package.documents, + ...this.fileManager.filesToUpload.files, + ]; + } + + @action + deselectFileForUpload(file) { + this.fileManager.deselectFileForUpload(file); + + this.args.package.documents = this.args.package.documents.filter( + (document) => document !== file, + ); + } + + @action + validateFilePresence() { + const validationResult = validateFileUpload({ message: 'One or more document uploads is required.' })( + 'documents', + this.fileManager.filesToUpload.files, + ); + + if (validationResult !== true) { + this.errorMessage = validationResult; + } else { + this.errorMessage = null; + } + } +} diff --git a/client/app/components/projects/projects-new-project-description.hbs b/client/app/components/projects/projects-new-project-description.hbs index ecd8a503..f2a33b31 100644 --- a/client/app/components/projects/projects-new-project-description.hbs +++ b/client/app/components/projects/projects-new-project-description.hbs @@ -1,16 +1,16 @@ {{#let @form as |form|}} - - - - Please replace information in the brackets to the best of your ability. - + + + + Please replace information in the brackets to the best of your ability. + - - + + {{/let}} diff --git a/client/app/models/artifact.js b/client/app/models/artifact.js index f6baabaf..56667272 100644 --- a/client/app/models/artifact.js +++ b/client/app/models/artifact.js @@ -33,6 +33,9 @@ export default class ArtifactModel extends Model { @belongsTo('project', { async: false }) project; + @belongsTo('project-new', { async: false }) + projectNew + @attr() dcpName; diff --git a/client/app/models/project-new.js b/client/app/models/project-new.js new file mode 100644 index 00000000..d49936a4 --- /dev/null +++ b/client/app/models/project-new.js @@ -0,0 +1,124 @@ +import Model, { attr, belongsTo } from '@ember-data/model'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import FileManager from '../services/file-manager'; + +export default class ProjectNew extends Model { + createFileQueue() { + if (this.fileManager) { + this.fileManager.existingFiles = this.documents; + } else { + const fileQueue = this.fileQueue.create(`artifact${this.id}`); + + this.fileManager = new FileManager( + this.id, + 'artifact', + this.documents, + [], + fileQueue, + this.session, + ); + } + } + + // Since file upload doesn't perform requests through + // an Ember Model save() process, it doesn't automatically + // hydrate the package.adapterError property. When an error occurs + // during upload we have to manually hydrate a custom error property + // to trigger the error box displayed to the user. + @tracked + fileUploadErrors = null; + + @service + session; + + @service + fileQueue; + + @belongsTo('projects', { async: false }) + projects; + + @attr('string', { + defaultValue: '', + }) + projectName; + + @attr('string', { + defaultValue: '', + }) + borough; + + @attr('string', { + defaultValue: '', + }) + applicantType; + + @attr('string', { + defaultValue: '', + }) + primaryContactFirstName; + + @attr('string', { + defaultValue: '', + }) + primaryContactLastName; + + @attr('string', { + defaultValue: '', + }) + primaryContactEmail; + + @attr('string', { + defaultValue: '', + }) + primaryContactPhone; + + @attr('string', { + defaultValue: '', + }) + applicantFirstName; + + @attr('string', { + defaultValue: '', + }) + applicantLastName; + + @attr('string', { + defaultValue: '', + }) + applicantEmail; + + @attr('string', { + defaultValue: '', + }) + applicantPhone; + + @attr('string', { + defaultValue: '', + }) + projectBrief; + + @attr('string', { + defaultValue: () => [], + }) + documents; + + async saveAttachedFiles(instanceId) { + try { + await this.fileManager.save(instanceId); + } catch (e) { + console.log('Error saving files: ', e); // eslint-disable-line no-console + + // See comment on the tracked fileUploadError property + // definition above. + this.fileUploadErrors = [ + { + code: 'UPLOAD_DOC_FAILED', + title: 'Failed to upload documents', + detail: + 'An error occured while uploading your documents. Please refresh and retry.', + }, + ]; + } + } +} diff --git a/client/app/routes/projects/new.js b/client/app/routes/projects/new.js index 731aa2d8..f49105d6 100644 --- a/client/app/routes/projects/new.js +++ b/client/app/routes/projects/new.js @@ -1,25 +1,21 @@ import Route from '@ember/routing/route'; import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; +import { inject as service } from '@ember/service'; export default class ProjectsNewRoute extends Route.extend( AuthenticatedRouteMixin, ) { authenticationRoute = '/'; + @service + store + async model() { - return { - projectName: '', - borough: '', - applicantType: '', - primaryContactFirstName: '', - primaryContactLastName: '', - primaryContactEmail: '', - primaryContactPhone: '', - applicantFirstName: '', - applicantLastName: '', - applicantEmail: '', - applicantPhone: '', - projectBrief: '', - }; + const projectsNewForm = this.store.createRecord('project-new'); + + projectsNewForm.id = `new${self.crypto.randomUUID()}`; + projectsNewForm.createFileQueue(); + + return projectsNewForm; } } diff --git a/client/app/services/file-manager.js b/client/app/services/file-manager.js index c516b586..e27c748a 100644 --- a/client/app/services/file-manager.js +++ b/client/app/services/file-manager.js @@ -12,7 +12,10 @@ export default class FileManager { filesToUpload, // EmberFileUpload Queue Object session, ) { - console.assert(entityType === 'package' || entityType === 'artifact', "entityType must be 'package' or 'artifact'"); + console.assert( + entityType === 'package' || entityType === 'artifact', + "entityType must be 'package' or 'artifact'", + ); this.recordId = recordId; this.entityType = entityType; @@ -61,18 +64,24 @@ export default class FileManager { this.numFilesToUpload -= 1; } - async uploadFiles() { + async uploadFiles(instanceId = this.recordId) { for (let i = 0; i < this.filesToUpload.files.length; i += 1) { - await this.filesToUpload.files[i].upload(`${ENV.host}/documents/${this.entityType}`, { // eslint-disable-line - fileKey: 'file', - headers: { - Authorization: `Bearer ${this.session.data.authenticated.access_token}`, + // eslint-disable-next-line no-await-in-loop + await this.filesToUpload.files[i].upload( + `${ENV.host}/documents/${this.entityType}`, + { + // eslint-disable-line + fileKey: 'file', + headers: { + Authorization: `Bearer ${this.session.data.authenticated.access_token}`, + }, + data: { + instanceId, + entityName: + this.entityType === 'artifact' ? 'dcp_artifacts' : 'dcp_package', + }, }, - data: { - instanceId: this.recordId, - entityName: this.entityType === 'artifact' ? 'dcp_artifacts' : 'dcp_package', - }, - }); + ); } } @@ -82,21 +91,24 @@ export default class FileManager { // TODO: If this is not possible, rework this to be a // POST request to a differently named endpoint, like // deleteDocument - return Promise.all(this.filesToDelete.map((file) => fetch( - `${ENV.host}/documents?serverRelativeUrl=${file.serverRelativeUrl}`, { - method: 'DELETE', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.session.data.authenticated.access_token}`, + return Promise.all( + this.filesToDelete.map((file) => fetch( + `${ENV.host}/documents?serverRelativeUrl=${file.serverRelativeUrl}`, + { + method: 'DELETE', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.session.data.authenticated.access_token}`, + }, }, - }, - ))); + )), + ); } - async save() { + async save(instanceId) { // See TODO at top of this file. - await this.uploadFiles(); + await this.uploadFiles(instanceId); await this.deleteFiles(); diff --git a/client/app/validations/submittable-projects-new-form.js b/client/app/validations/submittable-projects-new-form.js index 14603a18..498af0ad 100644 --- a/client/app/validations/submittable-projects-new-form.js +++ b/client/app/validations/submittable-projects-new-form.js @@ -3,6 +3,7 @@ import { validateLength, validatePresence, } from 'ember-changeset-validations/validators'; +import validateFileUpload from '../validators/validate-file-presence'; export default { primaryContactFirstName: [ @@ -137,4 +138,9 @@ export default { message: 'This field is required', }), ], + documents: [ + validateFileUpload({ + message: 'One or more document uploads is required.', + }), + ], }; diff --git a/client/app/validators/validate-file-presence.js b/client/app/validators/validate-file-presence.js new file mode 100644 index 00000000..293e41a0 --- /dev/null +++ b/client/app/validators/validate-file-presence.js @@ -0,0 +1,9 @@ +export default function validateFileUpload(options = {}) { + return (key, newValue) => { + if (!newValue || newValue.length === 0) { + return options.message || `${key} one or more document uploads is required.`; + } + + return true; + }; +} diff --git a/server/src/artifacts/artifacts.service.ts b/server/src/artifacts/artifacts.service.ts index 5d732ff1..d5d2fda9 100644 --- a/server/src/artifacts/artifacts.service.ts +++ b/server/src/artifacts/artifacts.service.ts @@ -7,6 +7,7 @@ import { ConfigService } from '../config/config.service'; @Injectable() export class ArtifactService { rerFiletypeUuid = ''; + letterFiletypeUuid= ''; constructor( private readonly crmService: CrmService, @@ -14,6 +15,7 @@ export class ArtifactService { private readonly config: ConfigService, ) { this.rerFiletypeUuid = this.config.get('RER_FILETYPE_UUID'); + this.letterFiletypeUuid = this.config.get('LETTER_FILETYPE_UUID'); } public async createEquityReport(projectId: string) { @@ -45,6 +47,35 @@ export class ArtifactService { return newArtifact; } + public async createProjectInitiationArtifacts(projectId: string) { + let newArtifact = null; + + try { + newArtifact = this.crmService.create('dcp_artifactses', { + dcp_name: `Project Initiation`, + dcp_isdcpinternal: false, + dcp_filecreator: 717170000, // Applicant + dcp_filecategory: 717170006, // Other + dcp_visibility: 717170002, // Applicant Only + 'dcp_applicantfiletype@odata.bind': `/dcp_filetypes(${this.letterFiletypeUuid})`, + ...(projectId + ? { 'dcp_project@odata.bind': `/dcp_projects(${projectId})` } + : {}), + }); + } catch (e) { + throw new HttpException( + { + code: 'CREATE_PROJECT_ARTIFACT_ERROR', + title: `Unable to create Project Initiation Artifact dcp_artifactses entity for project with UUID ${projectId}`, + detail: e, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + return newArtifact; + } + async getArtifactSharepointDocuments(relativeUrl: string, dcp_name: string) { if (relativeUrl) { try { diff --git a/server/src/crm/crm.service.ts b/server/src/crm/crm.service.ts index 5f0f0d8e..8ef9aba5 100644 --- a/server/src/crm/crm.service.ts +++ b/server/src/crm/crm.service.ts @@ -19,6 +19,7 @@ export class CrmService { crmUrlPath = ''; crmHost = ''; host = ''; + requestCounter = 0; constructor(private readonly config: ConfigService) { ADAL.ADAL_CONFIG = { @@ -62,7 +63,21 @@ export class CrmService { } async create(query, data, headers = {}) { - return this._create(query, data, headers); + try { + console.debug( + "LOGGER CRM create query", query, + "LOGGER CRM create data", data, + "LOGGER CRM create headers", headers, + ); + const requestStartTime = Date.now(); + const response = this._create(query, data, headers); + const requestEndTime = Date.now(); + console.debug(`LOGGER: LOGGER CRM create request took ${requestEndTime - requestStartTime} ms`); + return response; + + } catch (e) { + console.debug(`LOGGER error in CRM create: ${e}`); + } } async update(entity, guid, data, headers = {}) { diff --git a/server/src/projects/projects.controller.ts b/server/src/projects/projects.controller.ts index 40e2d0ae..ffcc65a3 100644 --- a/server/src/projects/projects.controller.ts +++ b/server/src/projects/projects.controller.ts @@ -38,6 +38,7 @@ import { INVOICE_ATTRS } from '../invoices/invoices.attrs'; 'team-members', 'contacts', 'milestones', + 'dcp_artifactsid', ], packages: { ref: 'dcp_packageid', @@ -82,6 +83,7 @@ import { INVOICE_ATTRS } from '../invoices/invoices.attrs'; @Controller('projects') export class ProjectsController { CRM_IMPOSTER_ID = ''; + requestCounter = 0; constructor( private readonly projectsService: ProjectsService, @@ -131,6 +133,7 @@ export class ProjectsController { @Post('/') async createProject(@Body() body) { + const requestStartTime = Date.now(); const allowedAttrs = pick(body, PROJECT_ATTRS) as { dcp_projectname: string; dcp_borough: string; @@ -148,6 +151,10 @@ export class ProjectsController { HttpStatus.NOT_FOUND, ); } + const requestEndTime = Date.now(); + this.requestCounter++; + console.log(`LOGGER: [Total Requests Made in the controller] ${this.requestCounter}`) + console.debug(`LOGGER: POST request in the controller to took ${requestEndTime - requestStartTime} ms`); return await this.projectsService.create(allowedAttrs); } diff --git a/server/src/projects/projects.module.ts b/server/src/projects/projects.module.ts index 97e4eb17..6614eaf9 100644 --- a/server/src/projects/projects.module.ts +++ b/server/src/projects/projects.module.ts @@ -6,10 +6,18 @@ import { ConfigModule } from '../config/config.module'; import { AuthModule } from '../auth/auth.module'; import { ProjectsController } from './projects.controller'; import { ProjectApplicantController } from './project-applicants/project-applicant.controller'; +import { ArtifactService } from '../artifacts/artifacts.service'; +import { SharepointModule } from '../sharepoint/sharepoint.module'; @Module({ - imports: [CrmModule, ConfigModule, ContactModule, AuthModule], - providers: [ProjectsService], + imports: [ + CrmModule, + SharepointModule, + ConfigModule, + ContactModule, + AuthModule, + ], + providers: [ProjectsService, ArtifactService], exports: [ProjectsService], controllers: [ProjectsController, ProjectApplicantController], }) diff --git a/server/src/projects/projects.service.ts b/server/src/projects/projects.service.ts index d1364fdd..86e86678 100644 --- a/server/src/projects/projects.service.ts +++ b/server/src/projects/projects.service.ts @@ -4,11 +4,13 @@ import { NycidService } from '../contact/nycid/nycid.service'; import { CrmService } from '../crm/crm.service'; import { overwriteCodesWithLabels } from '../_utils/overwrite-codes-with-labels'; import { MILESTONE_ATTRS, MILESTONE_NON_DATE_ATTRS } from './projects.attrs'; +import { ArtifactService } from '../artifacts/artifacts.service'; const APPLICANT_ACTIVE_STATUS_CODE = 1; const PROJECT_ACTIVE_STATE_CODE = 0; const PROJECT_VISIBILITY_APPLICANT_ONLY = 717170002; const PROJECT_VISIBILITY_GENERAL_PUBLIC = 717170003; +let requestCounter = 0; const PACKAGE_VISIBILITY = { APPLICANT_ONLY: 717170002, @@ -52,6 +54,7 @@ export class ProjectsService { constructor( private readonly crmService: CrmService, private readonly nycidService: NycidService, + private readonly artifactService: ArtifactService, ) {} public async findManyByContactId(contactId: string) { @@ -110,6 +113,8 @@ export class ProjectsService { _dcp_applicantadministrator_customer_value: string; }) { try { + const requestStartTime = Date.now(); + const data = { dcp_projectname: attributes.dcp_projectname, dcp_borough: attributes.dcp_borough, @@ -118,15 +123,28 @@ export class ProjectsService { 'dcp_applicant_customer_contact@odata.bind': `/contacts(${attributes._dcp_applicant_customer_value})`, 'dcp_applicantadministrator_customer_contact@odata.bind': `/contacts(${attributes._dcp_applicantadministrator_customer_value})`, }; - const { dcp_projectid } = await this.crmService.create( - 'dcp_projects', - data, - ); + + const project = await this.crmService.create('dcp_projects', data); + + const dcpProjectId = project['dcp_projectid']; + if (dcpProjectId === undefined) throw new Error('Failed to create project'); + + const artifact = + await this.artifactService.createProjectInitiationArtifacts(dcpProjectId); + + const dcpArtifactsId = artifact['dcp_artifactsid']; + if (dcpArtifactsId === undefined) throw new Error('Failed to create artifact for project'); + + const requestEndTime = Date.now(); + console.debug(`LOGGER: POST (service) request in the service to took ${requestEndTime - requestStartTime} ms`); + requestCounter++; + console.log(`LOGGER: [Total Requests Made in the service] ${requestCounter}`); return { - dcp_projectid, + dcp_projectid: dcpProjectId, + dcp_artifactsid: dcpArtifactsId, }; } catch (e) { - console.debug('error creating project', e); + console.error('(service) error creating project', e); throw new HttpException( 'Unable to create project', HttpStatus.INTERNAL_SERVER_ERROR,