<commit_before>/*
(c) Copyright 2016-2017 Hewlett Packard Enterprise Development LP

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

import Resource from './resource';
import { getDeviceName } from '../../../middleware/utils/lexer';

export default class ServerHardware extends Resource {

  constructor(oneViewResource) {
    if (oneViewResource) {
      super(oneViewResource);
      this.name = oneViewResource.name;
      this.formFactor = oneViewResource.formFactor;
      this.powerState = oneViewResource.powerState;
      this.serverProfileUri = oneViewResource.serverProfileUri;
      this.serverProfileHyperlink = oneViewResource.serverProfileHyperlink;
    }
  }

  buildSlackFields() {
    let fields = [];
    let hasProfile = false;
    for (const field in this) {
      if (this.__isNonDisplayField__(field) || !this[field]) {
        continue;
      }

      fields.push({
        title: this.camelCaseToTitleCase(field),
        short: true,
        value: this[field]
      });
    }
    if (this.serverProfileUri) {
      fields.push({
        title: 'Profile',
        short: true,
        value: '<' + this.serverProfileHyperlink + '|' + getDeviceName(this.serverProfileUri) + '>'
      });
      hasProfile = true;
    }
    if (!hasProfile) {
      fields.push({
        title: 'Profile',
        short: true,
        value: 'Available for deployment'
      });
    }
    return fields;
  }

  buildHipChatOutput() {
    let output = '';
    let hasProfile = false;
    for (const field in this) {
      if (this.__isNonDisplayField__(field) || !this[field]) {
        continue;
      }
      output += '\t\u2022 ' + this.camelCaseToTitleCase(field) + ': ' + this[field] + '\n';
    }
    if (this.serverProfileUri) {
      output += '\t\u2022 Profile: ' +  getDeviceName(this.serverProfileUri) + '\n';
      hasProfile = true;
    }
    if (!hasProfile) {
      output += '\t\u2022 Profile: Available for deployment\n';
    }
    //Add status to output only for HipChat
    output += '\t\u2022 Status: ' +  this.status + '\n';

    return output;
  }

  __isNonDisplayField__(field){
    var nonDisplayFields = ['name', 'type', 'status', 'serverprofileuri', 'serverprofilehyperlink', 'hyperlink'];
    return nonDisplayFields.includes(field.toLowerCase());
  }
}
<commit_msg>This resolves issue #68<commit_after>/*
(c) Copyright 2016-2017 Hewlett Packard Enterprise Development LP

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

import Resource from './resource';
import { getDeviceName } from '../../../middleware/utils/lexer';

export default class ServerHardware extends Resource {

  constructor(oneViewResource) {
    if (oneViewResource) {
      super(oneViewResource);
      this.name = oneViewResource.name;
      this.model = oneViewResource.model;
      this.powerState = oneViewResource.powerState;
      this.serverProfileUri = oneViewResource.serverProfileUri;
      this.serverProfileHyperlink = oneViewResource.serverProfileHyperlink;
    }
  }

  buildSlackFields() {
    let fields = [];
    let hasProfile = false;
    for (const field in this) {
      if (this.__isNonDisplayField__(field) || !this[field]) {
        continue;
      }

      fields.push({
        title: this.camelCaseToTitleCase(field),
        short: true,
        value: this[field]
      });
    }
    if (this.serverProfileUri) {
      fields.push({
        title: 'Profile',
        short: true,
        value: '<' + this.serverProfileHyperlink + '|' + getDeviceName(this.serverProfileUri) + '>'
      });
      hasProfile = true;
    }
    if (!hasProfile) {
      fields.push({
        title: 'Profile',
        short: true,
        value: 'Available for deployment'
      });
    }
    return fields;
  }

  buildHipChatOutput() {
    let output = '';
    let hasProfile = false;
    for (const field in this) {
      if (this.__isNonDisplayField__(field) || !this[field]) {
        continue;
      }
      output += '\t\u2022 ' + this.camelCaseToTitleCase(field) + ': ' + this[field] + '\n';
    }
    if (this.serverProfileUri) {
      output += '\t\u2022 Profile: ' +  getDeviceName(this.serverProfileUri) + '\n';
      hasProfile = true;
    }
    if (!hasProfile) {
      output += '\t\u2022 Profile: Available for deployment\n';
    }
    //Add status to output only for HipChat
    output += '\t\u2022 Status: ' +  this.status + '\n';

    return output;
  }

  __isNonDisplayField__(field){
    var nonDisplayFields = ['name', 'type', 'status', 'serverprofileuri', 'serverprofilehyperlink', 'hyperlink'];
    return nonDisplayFields.includes(field.toLowerCase());
  }
}
<|endoftext|>


<commit_before>// Server / Platform  Imports
import { LightningElement, api, track } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import saveAndDryRunDataImport from '@salesforce/apex/GE_GiftEntryController.saveAndDryRunDataImport';
import sendPurchaseRequest from '@salesforce/apex/GE_GiftEntryController.sendPurchaseRequest';
import upsertDataImport from '@salesforce/apex/GE_GiftEntryController.upsertDataImport';
import submitDataImportToBDI from '@salesforce/apex/GE_GiftEntryController.submitDataImportToBDI';

// Function / Class imports
import { showToast } from 'c/utilTemplateBuilder';
import { isNotEmpty } from 'c/utilCommon';
import GeLabelService from 'c/geLabelService';

// Schema Imports
import DATA_IMPORT_BATCH_OBJECT from '@salesforce/schema/DataImportBatch__c';
import DI_PAYMENT_AUTHORIZE_TOKEN_FIELD from '@salesforce/schema/DataImport__c.Payment_Authorization_Token__c';
import DI_PAYMENT_STATUS_FIELD from '@salesforce/schema/DataImport__c.Payment_Status__c';
import DI_PAYMENT_DECLINED_REASON_FIELD from '@salesforce/schema/DataImport__c.Payment_Declined_Reason__c';
import DI_PAYMENT_METHOD_FIELD from '@salesforce/schema/DataImport__c.Payment_Method__c';
import DI_DONATION_AMOUNT_FIELD from '@salesforce/schema/DataImport__c.Donation_Amount__c';
import DI_DONATION_CAMPAIGN_NAME_FIELD from '@salesforce/schema/DataImport__c.Donation_Campaign_Name__c';

// Constants
const PAYMENT_STATUS__C = DI_PAYMENT_STATUS_FIELD.fieldApiName;
const PAYMENT_DECLINED_REASON__C = DI_PAYMENT_DECLINED_REASON_FIELD.fieldApiName;
const PAYMENT_AUTHORIZE_TOKEN__C = DI_PAYMENT_AUTHORIZE_TOKEN_FIELD.fieldApiName;
const PAYMENT_METHOD__C = DI_PAYMENT_METHOD_FIELD.fieldApiName;
const DONATION_AMOUNT__C = DI_DONATION_AMOUNT_FIELD.fieldApiName;
const DONATION_CAMPAIGN_NAME__C = DI_DONATION_CAMPAIGN_NAME_FIELD.fieldApiName;
const TOKENIZE_TIMEOUT = 10000; // 10 seconds
const PAYMENT_TRANSACTION_STATUS_ENUM = Object.freeze({
    PENDING: 'PENDING',
    AUTHORIZED: 'AUTHORIZED',
    CANCELED: 'CANCELED',
    CAPTURED: 'CAPTURED',
    DECLINED: 'DECLINED',
    NONRETRYABLEERROR: 'NONRETRYABLEERROR',
    RETRYABLEERROR: 'RETRYABLEERROR',
    REFUNDISSUED: 'REFUNDISSUED'
});

export default class GeGiftEntryFormApp extends NavigationMixin(LightningElement) {

    // Expose custom labels to template
    CUSTOM_LABELS = GeLabelService.CUSTOM_LABELS;

    @api recordId;
    @api sObjectName;

    @track isPermissionError;
    @track loadingText = this.CUSTOM_LABELS.geTextSaving;

    dataImportRecord = {};
    errorCallback;

    get isBatchMode() {
        return this.sObjectName &&
            this.sObjectName === DATA_IMPORT_BATCH_OBJECT.objectApiName;
    }

    /*******************************************************************************
    * @description Receives a 'submit' event from geFormRenderer and proceeds down
    * the Batch or Single save paths depending on the current app mode.
    *
    * @param {object} event: Custom Event containing the Data Import record and
    * potentially other objects (booleans, callbacks, etc).
    */
    handleSubmit(event) {
        // Callback received from geFormRenderer. Provides functions that
        // toggle the form save button, toggle the lightning spinner, and displays aura exceptions.
        this.errorCallback = event.detail.errorCallback;

        try {
            if (this.isBatchMode) {
                this.batchGiftSubmit(event);
            } else {
                this.singleGiftSubmit(event);
            }
        } catch (error) {
            this.errorCallback(error);
        }
    }

    /*******************************************************************************
    * @description Handles a batch gift entry submit. Saves a Data Import record,
    * runs dry run, and renders the Data Import in the Batch Gift Table.
    *
    * @param {object} event: Custom Event containing the Data Import record and a
    * callback for handling and displaying errors in the form.
    */
    batchGiftSubmit(event) {
        const table = this.template.querySelector('c-ge-batch-gift-entry-table');
        this.dataImportRecord = event.detail.dataImportRecord;
        saveAndDryRunDataImport({ batchId: this.recordId, dataImport: this.dataImportRecord })
            .then((result) => {
                let dataImportModel = JSON.parse(result);
                Object.assign(dataImportModel.dataImportRows[0],
                    dataImportModel.dataImportRows[0].record);
                table.upsertData(dataImportModel.dataImportRows[0], 'Id');
                table.setTotalCount(dataImportModel.totalCountOfRows);
                table.setTotalAmount(dataImportModel.totalAmountOfRows);
                event.detail.success(); //Re-enable the Save button
            })
            .catch(error => {
                event.detail.error(error);
            });
    }

    /*******************************************************************************
    * @description Handles a single gift entry submit. Saves a Data Import record,
    * makes an elevate payment if needed, and processes the Data Import through
    * BDI.
    *
    * @param {object} event: Custom Event containing the Data Import record and a
    * callback for handling and displaying errors in the form.
    */
    singleGiftSubmit = async (event) => {
        let { inMemoryDataImport } = event.detail;
        this.hasUserSelectedDonation = event.detail.hasUserSelectedDonation;

        try {
            await this.saveDataImport(inMemoryDataImport);

            const hasPaymentToProcess = this.dataImportRecord[PAYMENT_AUTHORIZE_TOKEN__C];
            if (hasPaymentToProcess) {
                await this.processPayment();
            }

            await this.processDataImport();
        } catch (error) {
            this.errorCallback(error);
        }
    }

    /*******************************************************************************
    * @description Upserts the provided data import record. Attempts to retrieve
    * an Elevate token for a purchase call if needed.
    *
    * @param {object} inMemoryDataImport: DataImport__c object built from the form
    * fields.
    *
    * @return {object} dataImportRecord: A DataImport__c record
    */
    saveDataImport = async (inMemoryDataImport) => {
        if (this.dataImportRecord.Id) {
            this.loadingText = this.CUSTOM_LABELS.geTextUpdating;
            inMemoryDataImport = this.prepareInMemoryDataImportForUpdate(inMemoryDataImport);
        } else {
            this.loadingText = this.CUSTOM_LABELS.geTextSaving;
        }

        this.dataImportRecord = await upsertDataImport({ dataImport: inMemoryDataImport });
    };

    /*******************************************************************************
    * @description Re-apply Data Import id and relevant payment/elevate fields.
    * The inMemoryDataImport is built new from the form on every save click. We
    * need to catch it up with the correct id to make sure we update instead of
    * inserting a new record on re-save attempts.
    *
    * @param {object} inMemoryDataImport: DataImport__c object built from the form
    * fields.
    *
    * @return {object} inMemoryDataImport: DataImport__c object built from the form
    * fields.
    */
    prepareInMemoryDataImportForUpdate(inMemoryDataImport) {
        inMemoryDataImport.Id = this.dataImportRecord.Id;
        inMemoryDataImport[PAYMENT_METHOD__C] = this.dataImportRecord[PAYMENT_METHOD__C];
        inMemoryDataImport[PAYMENT_STATUS__C] = this.dataImportRecord[PAYMENT_STATUS__C];
        inMemoryDataImport[PAYMENT_DECLINED_REASON__C] =
            this.dataImportRecord[PAYMENT_DECLINED_REASON__C];

        return inMemoryDataImport;
    }
    /*******************************************************************************
    * @description Method attempts to make a purchase call to Payment
    * Services. Immediately attempts to the charge the card provided in the Payment
    * Services iframe (GE_TokenizeCard).
    */
    processPayment = async () => {
        this.loadingText = this.CUSTOM_LABELS.geTextChargingCard;

        const isReadyToCharge = this.checkPaymentTransactionStatus(this.dataImportRecord[PAYMENT_STATUS__C]);
        if (isReadyToCharge) {

            const purchaseResponse = await this.makePurchaseCall();
            if (purchaseResponse) {

                this.dataImportRecord[PAYMENT_STATUS__C] = this.getPaymentStatus(purchaseResponse);
                this.dataImportRecord[PAYMENT_DECLINED_REASON__C] =
                    this.getPaymentDeclinedReason(purchaseResponse);

                this.dataImportRecord = await upsertDataImport({ dataImport: this.dataImportRecord });

                const isFailedPurchase = purchaseResponse.statusCode !== 201;
                if (isFailedPurchase) {
                    let errors = this.getFailedPurchaseMessage(purchaseResponse);
                    throw new Error(errors);
                }
            }
        }
    }

    /*******************************************************************************
    * @description Method checks the current payment transaction's status and
    * returns true if the card is in a 'chargeable' status.
    *
    * @param {string} paymentStatus: Payment transaction status
    *
    * @return {boolean}: True if card is in a 'chargeable' status
    */
    checkPaymentTransactionStatus = (paymentStatus) => {
        switch (paymentStatus) {
            case PAYMENT_TRANSACTION_STATUS_ENUM.PENDING: return false;
            case PAYMENT_TRANSACTION_STATUS_ENUM.AUTHORIZED: return false;
            case PAYMENT_TRANSACTION_STATUS_ENUM.CANCELED: return false;
            case PAYMENT_TRANSACTION_STATUS_ENUM.CAPTURED: return false;
            case PAYMENT_TRANSACTION_STATUS_ENUM.DECLINED: return true;
            case PAYMENT_TRANSACTION_STATUS_ENUM.NONRETRYABLEERROR: return false;
            case PAYMENT_TRANSACTION_STATUS_ENUM.RETRYABLEERROR: return true;
            case PAYMENT_TRANSACTION_STATUS_ENUM.REFUNDISSUED: return false;
            default: return true;
        }
    }

    /*******************************************************************************
    * @description Posts an http request through the `sendPurchaseRequest` apex
    * method and parses the response.
    *
    * @return {object} response: An http response object
    */
    makePurchaseCall = async () => {
        let purchaseResponseString = await sendPurchaseRequest({
            requestBodyParameters: this.buildRequestBodyParameters()
        });
        let response = JSON.parse(purchaseResponseString);
        response.body = JSON.parse(response.body);

        return response;
    }

    /*******************************************************************************
    * @description Builds parts of the purchase request body that requires data
    * from the Data Import record upfront. We pass this into the `sendPurchaseRequest`
    * method and is eventually merged in with the rest of the purchase request body.
    *
    * @return {object}: Object that we can deserialize and apply to the purchase
    * request body in apex.
    */
    buildRequestBodyParameters() {
        const names = this.getCardholderNames();
        const firstName = isNotEmpty(names.firstName) ? names.firstName : names.accountName;
        const lastName = isNotEmpty(names.lastName) ? names.lastName : names.accountName;
        const metadata = {
            campaignCode: this.dataImportRecord[DONATION_CAMPAIGN_NAME__C]
        }

        return JSON.stringify({
            amount: this.dataImportRecord[DONATION_AMOUNT__C],
            email: 'test@test.test',
            firstName: firstName,
            lastName: lastName,
            metadata: metadata,
            paymentMethodToken: this.dataImportRecord[PAYMENT_AUTHORIZE_TOKEN__C],
        })
    }

    /*******************************************************************************
    * @description Queries for the geFormRenderer component and retrieves the
    * fabricated cardholder names object.
    *
    * @return {object}: Object containing firstName, lastName, and accountName
    * from the form.
    */
    getCardholderNames() {
        const renderer = this.template.querySelector('c-ge-form-renderer');
        return renderer.fabricatedCardholderNames;
    }

    /*******************************************************************************
    * @description Get the value for DataImport__c.Payment_Status__c from the
    * purchase call response.
    *
    * @param {object} response: Http response object
    *
    * @return {string}: Status of the payment charge request
    */
    getPaymentStatus(response) {
        return response.body.status || response.status || this.CUSTOM_LABELS.commonUnknownError;
    }

    /*******************************************************************************
    * @description Get the value for DataImport__c.Payment_Declined_Reason__c from
    * the purchase call response.
    *
    * @param {object} response: Http response object
    *
    * @return {string}: Reason the payment was declined
    */
    getPaymentDeclinedReason(response) {
        const isSuccessfulPurchase = response.statusCode === 201;
        return isSuccessfulPurchase ? null : JSON.stringify(response.body);
    }

    /*******************************************************************************
    * @description Get the message or errors from a failed purchase call.
    *
    * @param {object} response: Http response object
    *
    * @return {string}: Message from a failed purchase call response
    */
    getFailedPurchaseMessage(response) {
        // For some reason the key in the body object for 'Message'
        // in the response we receive from Elevate is capitalized.
        // Also checking for lowercase M in message in case they fix it.
        return response.body.Message ||
            response.body.message ||
            response.body.errors.map(error => error.message).join(', ') ||
            this.CUSTOM_LABELS.commonUnknownError;
    }

    /*******************************************************************************
    * @description Sends the Data Import into BDI for processing and navigates to
    * the opportunity record detail page on success.
    */
    processDataImport = async () => {
        this.loadingText = this.CUSTOM_LABELS.geTextProcessing;

        submitDataImportToBDI({ diRecord: this.dataImportRecord, updateGift: this.hasUserSelectedDonation })
            .then(opportunityId => {
                this.loadingText = this.CUSTOM_LABELS.geTextNavigateToOpportunity;
                this.navigateToRecordPage(opportunityId);
            })
            .catch(error => {
                // TODO: Placeholder method for handling the scenario in which
                // we've charged a card successfully, but BDI processing failed.
                this.doSomethingLoud(error);
            });
    }

    // TODO: Placeholder method, remove/replace in work item to better handle purchase call errors
    doSomethingLoud(error) {
        this.errorCallback(error);
        // TODO: Potentially have to check for other types of status that MAY
        // indicate a charge could still occur (pending, authorized, etc)
        if (this.dataImportRecord[PAYMENT_STATUS__C] === PAYMENT_TRANSACTION_STATUS_ENUM.CAPTURED) {
            showToast('HEY!',
                'Card was charged. Please fix the outstanding errors and try again.',
                'error',
                'sticky');
        }
    }

    handleSectionsRetrieved(event) {
        const formSections = event.target.sections;
        const table = this.template.querySelector('c-ge-batch-gift-entry-table');
        table.handleSectionsRetrieved(formSections);
    }

    handleBatchDryRun() {
        //toggle the spinner on the form
        const form = this.template.querySelector('c-ge-form-renderer');
        const toggleSpinner = function () {
            form.showSpinner = !form.showSpinner
        };
        form.showSpinner = true;

        const table = this.template.querySelector('c-ge-batch-gift-entry-table');
        table.runBatchDryRun(toggleSpinner);
    }

    handleLoadData(event) {
        const form = this.template.querySelector('c-ge-form-renderer');
        form.reset();
        form.load(event.detail);
    }

    handlePermissionErrors() {
        this.isPermissionError = true;
    }
    handleEditBatch() {
        this.dispatchEvent(new CustomEvent('editbatch'));
    }

    handleReviewDonationsModal(event) {
        this.dispatchEvent(new CustomEvent('togglereviewdonationsmodal', { detail: event.detail }));
    }

    navigateToRecordPage(recordId) {
        this[NavigationMixin.Navigate]({
            type: 'standard__recordPage',
            attributes: {
                recordId: recordId,
                actionName: 'view'
            }
        });
    }
}
<commit_msg>remove unused constant<commit_after>// Server / Platform  Imports
import { LightningElement, api, track } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import saveAndDryRunDataImport from '@salesforce/apex/GE_GiftEntryController.saveAndDryRunDataImport';
import sendPurchaseRequest from '@salesforce/apex/GE_GiftEntryController.sendPurchaseRequest';
import upsertDataImport from '@salesforce/apex/GE_GiftEntryController.upsertDataImport';
import submitDataImportToBDI from '@salesforce/apex/GE_GiftEntryController.submitDataImportToBDI';

// Function / Class imports
import { showToast } from 'c/utilTemplateBuilder';
import { isNotEmpty } from 'c/utilCommon';
import GeLabelService from 'c/geLabelService';

// Schema Imports
import DATA_IMPORT_BATCH_OBJECT from '@salesforce/schema/DataImportBatch__c';
import DI_PAYMENT_AUTHORIZE_TOKEN_FIELD from '@salesforce/schema/DataImport__c.Payment_Authorization_Token__c';
import DI_PAYMENT_STATUS_FIELD from '@salesforce/schema/DataImport__c.Payment_Status__c';
import DI_PAYMENT_DECLINED_REASON_FIELD from '@salesforce/schema/DataImport__c.Payment_Declined_Reason__c';
import DI_PAYMENT_METHOD_FIELD from '@salesforce/schema/DataImport__c.Payment_Method__c';
import DI_DONATION_AMOUNT_FIELD from '@salesforce/schema/DataImport__c.Donation_Amount__c';
import DI_DONATION_CAMPAIGN_NAME_FIELD from '@salesforce/schema/DataImport__c.Donation_Campaign_Name__c';

// Constants
const PAYMENT_STATUS__C = DI_PAYMENT_STATUS_FIELD.fieldApiName;
const PAYMENT_DECLINED_REASON__C = DI_PAYMENT_DECLINED_REASON_FIELD.fieldApiName;
const PAYMENT_AUTHORIZE_TOKEN__C = DI_PAYMENT_AUTHORIZE_TOKEN_FIELD.fieldApiName;
const PAYMENT_METHOD__C = DI_PAYMENT_METHOD_FIELD.fieldApiName;
const DONATION_AMOUNT__C = DI_DONATION_AMOUNT_FIELD.fieldApiName;
const DONATION_CAMPAIGN_NAME__C = DI_DONATION_CAMPAIGN_NAME_FIELD.fieldApiName;
const PAYMENT_TRANSACTION_STATUS_ENUM = Object.freeze({
    PENDING: 'PENDING',
    AUTHORIZED: 'AUTHORIZED',
    CANCELED: 'CANCELED',
    CAPTURED: 'CAPTURED',
    DECLINED: 'DECLINED',
    NONRETRYABLEERROR: 'NONRETRYABLEERROR',
    RETRYABLEERROR: 'RETRYABLEERROR',
    REFUNDISSUED: 'REFUNDISSUED'
});

export default class GeGiftEntryFormApp extends NavigationMixin(LightningElement) {

    // Expose custom labels to template
    CUSTOM_LABELS = GeLabelService.CUSTOM_LABELS;

    @api recordId;
    @api sObjectName;

    @track isPermissionError;
    @track loadingText = this.CUSTOM_LABELS.geTextSaving;

    dataImportRecord = {};
    errorCallback;

    get isBatchMode() {
        return this.sObjectName &&
            this.sObjectName === DATA_IMPORT_BATCH_OBJECT.objectApiName;
    }

    /*******************************************************************************
    * @description Receives a 'submit' event from geFormRenderer and proceeds down
    * the Batch or Single save paths depending on the current app mode.
    *
    * @param {object} event: Custom Event containing the Data Import record and
    * potentially other objects (booleans, callbacks, etc).
    */
    handleSubmit(event) {
        // Callback received from geFormRenderer. Provides functions that
        // toggle the form save button, toggle the lightning spinner, and displays aura exceptions.
        this.errorCallback = event.detail.errorCallback;

        try {
            if (this.isBatchMode) {
                this.batchGiftSubmit(event);
            } else {
                this.singleGiftSubmit(event);
            }
        } catch (error) {
            this.errorCallback(error);
        }
    }

    /*******************************************************************************
    * @description Handles a batch gift entry submit. Saves a Data Import record,
    * runs dry run, and renders the Data Import in the Batch Gift Table.
    *
    * @param {object} event: Custom Event containing the Data Import record and a
    * callback for handling and displaying errors in the form.
    */
    batchGiftSubmit(event) {
        const table = this.template.querySelector('c-ge-batch-gift-entry-table');
        this.dataImportRecord = event.detail.dataImportRecord;
        saveAndDryRunDataImport({ batchId: this.recordId, dataImport: this.dataImportRecord })
            .then((result) => {
                let dataImportModel = JSON.parse(result);
                Object.assign(dataImportModel.dataImportRows[0],
                    dataImportModel.dataImportRows[0].record);
                table.upsertData(dataImportModel.dataImportRows[0], 'Id');
                table.setTotalCount(dataImportModel.totalCountOfRows);
                table.setTotalAmount(dataImportModel.totalAmountOfRows);
                event.detail.success(); //Re-enable the Save button
            })
            .catch(error => {
                event.detail.error(error);
            });
    }

    /*******************************************************************************
    * @description Handles a single gift entry submit. Saves a Data Import record,
    * makes an elevate payment if needed, and processes the Data Import through
    * BDI.
    *
    * @param {object} event: Custom Event containing the Data Import record and a
    * callback for handling and displaying errors in the form.
    */
    singleGiftSubmit = async (event) => {
        let { inMemoryDataImport } = event.detail;
        this.hasUserSelectedDonation = event.detail.hasUserSelectedDonation;

        try {
            await this.saveDataImport(inMemoryDataImport);

            const hasPaymentToProcess = this.dataImportRecord[PAYMENT_AUTHORIZE_TOKEN__C];
            if (hasPaymentToProcess) {
                await this.processPayment();
            }

            await this.processDataImport();
        } catch (error) {
            this.errorCallback(error);
        }
    }

    /*******************************************************************************
    * @description Upserts the provided data import record. Attempts to retrieve
    * an Elevate token for a purchase call if needed.
    *
    * @param {object} inMemoryDataImport: DataImport__c object built from the form
    * fields.
    *
    * @return {object} dataImportRecord: A DataImport__c record
    */
    saveDataImport = async (inMemoryDataImport) => {
        if (this.dataImportRecord.Id) {
            this.loadingText = this.CUSTOM_LABELS.geTextUpdating;
            inMemoryDataImport = this.prepareInMemoryDataImportForUpdate(inMemoryDataImport);
        } else {
            this.loadingText = this.CUSTOM_LABELS.geTextSaving;
        }

        this.dataImportRecord = await upsertDataImport({ dataImport: inMemoryDataImport });
    };

    /*******************************************************************************
    * @description Re-apply Data Import id and relevant payment/elevate fields.
    * The inMemoryDataImport is built new from the form on every save click. We
    * need to catch it up with the correct id to make sure we update instead of
    * inserting a new record on re-save attempts.
    *
    * @param {object} inMemoryDataImport: DataImport__c object built from the form
    * fields.
    *
    * @return {object} inMemoryDataImport: DataImport__c object built from the form
    * fields.
    */
    prepareInMemoryDataImportForUpdate(inMemoryDataImport) {
        inMemoryDataImport.Id = this.dataImportRecord.Id;
        inMemoryDataImport[PAYMENT_METHOD__C] = this.dataImportRecord[PAYMENT_METHOD__C];
        inMemoryDataImport[PAYMENT_STATUS__C] = this.dataImportRecord[PAYMENT_STATUS__C];
        inMemoryDataImport[PAYMENT_DECLINED_REASON__C] =
            this.dataImportRecord[PAYMENT_DECLINED_REASON__C];

        return inMemoryDataImport;
    }
    /*******************************************************************************
    * @description Method attempts to make a purchase call to Payment
    * Services. Immediately attempts to the charge the card provided in the Payment
    * Services iframe (GE_TokenizeCard).
    */
    processPayment = async () => {
        this.loadingText = this.CUSTOM_LABELS.geTextChargingCard;

        const isReadyToCharge = this.checkPaymentTransactionStatus(this.dataImportRecord[PAYMENT_STATUS__C]);
        if (isReadyToCharge) {

            const purchaseResponse = await this.makePurchaseCall();
            if (purchaseResponse) {

                this.dataImportRecord[PAYMENT_STATUS__C] = this.getPaymentStatus(purchaseResponse);
                this.dataImportRecord[PAYMENT_DECLINED_REASON__C] =
                    this.getPaymentDeclinedReason(purchaseResponse);

                this.dataImportRecord = await upsertDataImport({ dataImport: this.dataImportRecord });

                const isFailedPurchase = purchaseResponse.statusCode !== 201;
                if (isFailedPurchase) {
                    let errors = this.getFailedPurchaseMessage(purchaseResponse);
                    throw new Error(errors);
                }
            }
        }
    }

    /*******************************************************************************
    * @description Method checks the current payment transaction's status and
    * returns true if the card is in a 'chargeable' status.
    *
    * @param {string} paymentStatus: Payment transaction status
    *
    * @return {boolean}: True if card is in a 'chargeable' status
    */
    checkPaymentTransactionStatus = (paymentStatus) => {
        switch (paymentStatus) {
            case PAYMENT_TRANSACTION_STATUS_ENUM.PENDING: return false;
            case PAYMENT_TRANSACTION_STATUS_ENUM.AUTHORIZED: return false;
            case PAYMENT_TRANSACTION_STATUS_ENUM.CANCELED: return false;
            case PAYMENT_TRANSACTION_STATUS_ENUM.CAPTURED: return false;
            case PAYMENT_TRANSACTION_STATUS_ENUM.DECLINED: return true;
            case PAYMENT_TRANSACTION_STATUS_ENUM.NONRETRYABLEERROR: return false;
            case PAYMENT_TRANSACTION_STATUS_ENUM.RETRYABLEERROR: return true;
            case PAYMENT_TRANSACTION_STATUS_ENUM.REFUNDISSUED: return false;
            default: return true;
        }
    }

    /*******************************************************************************
    * @description Posts an http request through the `sendPurchaseRequest` apex
    * method and parses the response.
    *
    * @return {object} response: An http response object
    */
    makePurchaseCall = async () => {
        let purchaseResponseString = await sendPurchaseRequest({
            requestBodyParameters: this.buildRequestBodyParameters()
        });
        let response = JSON.parse(purchaseResponseString);
        response.body = JSON.parse(response.body);

        return response;
    }

    /*******************************************************************************
    * @description Builds parts of the purchase request body that requires data
    * from the Data Import record upfront. We pass this into the `sendPurchaseRequest`
    * method and is eventually merged in with the rest of the purchase request body.
    *
    * @return {object}: Object that we can deserialize and apply to the purchase
    * request body in apex.
    */
    buildRequestBodyParameters() {
        const names = this.getCardholderNames();
        const firstName = isNotEmpty(names.firstName) ? names.firstName : names.accountName;
        const lastName = isNotEmpty(names.lastName) ? names.lastName : names.accountName;
        const metadata = {
            campaignCode: this.dataImportRecord[DONATION_CAMPAIGN_NAME__C]
        }

        return JSON.stringify({
            amount: this.dataImportRecord[DONATION_AMOUNT__C],
            email: 'test@test.test',
            firstName: firstName,
            lastName: lastName,
            metadata: metadata,
            paymentMethodToken: this.dataImportRecord[PAYMENT_AUTHORIZE_TOKEN__C],
        })
    }

    /*******************************************************************************
    * @description Queries for the geFormRenderer component and retrieves the
    * fabricated cardholder names object.
    *
    * @return {object}: Object containing firstName, lastName, and accountName
    * from the form.
    */
    getCardholderNames() {
        const renderer = this.template.querySelector('c-ge-form-renderer');
        return renderer.fabricatedCardholderNames;
    }

    /*******************************************************************************
    * @description Get the value for DataImport__c.Payment_Status__c from the
    * purchase call response.
    *
    * @param {object} response: Http response object
    *
    * @return {string}: Status of the payment charge request
    */
    getPaymentStatus(response) {
        return response.body.status || response.status || this.CUSTOM_LABELS.commonUnknownError;
    }

    /*******************************************************************************
    * @description Get the value for DataImport__c.Payment_Declined_Reason__c from
    * the purchase call response.
    *
    * @param {object} response: Http response object
    *
    * @return {string}: Reason the payment was declined
    */
    getPaymentDeclinedReason(response) {
        const isSuccessfulPurchase = response.statusCode === 201;
        return isSuccessfulPurchase ? null : JSON.stringify(response.body);
    }

    /*******************************************************************************
    * @description Get the message or errors from a failed purchase call.
    *
    * @param {object} response: Http response object
    *
    * @return {string}: Message from a failed purchase call response
    */
    getFailedPurchaseMessage(response) {
        // For some reason the key in the body object for 'Message'
        // in the response we receive from Elevate is capitalized.
        // Also checking for lowercase M in message in case they fix it.
        return response.body.Message ||
            response.body.message ||
            response.body.errors.map(error => error.message).join(', ') ||
            this.CUSTOM_LABELS.commonUnknownError;
    }

    /*******************************************************************************
    * @description Sends the Data Import into BDI for processing and navigates to
    * the opportunity record detail page on success.
    */
    processDataImport = async () => {
        this.loadingText = this.CUSTOM_LABELS.geTextProcessing;

        submitDataImportToBDI({ diRecord: this.dataImportRecord, updateGift: this.hasUserSelectedDonation })
            .then(opportunityId => {
                this.loadingText = this.CUSTOM_LABELS.geTextNavigateToOpportunity;
                this.navigateToRecordPage(opportunityId);
            })
            .catch(error => {
                // TODO: Placeholder method for handling the scenario in which
                // we've charged a card successfully, but BDI processing failed.
                this.doSomethingLoud(error);
            });
    }

    // TODO: Placeholder method, remove/replace in work item to better handle purchase call errors
    doSomethingLoud(error) {
        this.errorCallback(error);
        // TODO: Potentially have to check for other types of status that MAY
        // indicate a charge could still occur (pending, authorized, etc)
        if (this.dataImportRecord[PAYMENT_STATUS__C] === PAYMENT_TRANSACTION_STATUS_ENUM.CAPTURED) {
            showToast('HEY!',
                'Card was charged. Please fix the outstanding errors and try again.',
                'error',
                'sticky');
        }
    }

    handleSectionsRetrieved(event) {
        const formSections = event.target.sections;
        const table = this.template.querySelector('c-ge-batch-gift-entry-table');
        table.handleSectionsRetrieved(formSections);
    }

    handleBatchDryRun() {
        //toggle the spinner on the form
        const form = this.template.querySelector('c-ge-form-renderer');
        const toggleSpinner = function () {
            form.showSpinner = !form.showSpinner
        };
        form.showSpinner = true;

        const table = this.template.querySelector('c-ge-batch-gift-entry-table');
        table.runBatchDryRun(toggleSpinner);
    }

    handleLoadData(event) {
        const form = this.template.querySelector('c-ge-form-renderer');
        form.reset();
        form.load(event.detail);
    }

    handlePermissionErrors() {
        this.isPermissionError = true;
    }
    handleEditBatch() {
        this.dispatchEvent(new CustomEvent('editbatch'));
    }

    handleReviewDonationsModal(event) {
        this.dispatchEvent(new CustomEvent('togglereviewdonationsmodal', { detail: event.detail }));
    }

    navigateToRecordPage(recordId) {
        this[NavigationMixin.Navigate]({
            type: 'standard__recordPage',
            attributes: {
                recordId: recordId,
                actionName: 'view'
            }
        });
    }
}
<|endoftext|>


<commit_before>var customLaunchers = {
    // Chrome
    "sl_chrome_43": {
        base: "SauceLabs",
        browserName: "chrome",
        version: "43"
    },
    "sl_chrome_42": {
        base: "SauceLabs",
        browserName: "chrome",
        version: "42"
    },
    "sl_chrome_36": {
        base: "SauceLabs",
        browserName: "chrome",
        version: "36"
    },
    "sl_chrome_31": {
        base: "SauceLabs",
        browserName: "chrome",
        version: "31"
    },

    // Firefox
    "sl_firefox_37": {
        base: "SauceLabs",
        browserName: "firefox",
        version: "37"
    },
    "sl_firefox_38": {
        base: "SauceLabs",
        browserName: "firefox",
        version: "38"
    },

    // Internet Explorer
    "sl_ie_9": {
        base: "SauceLabs",
        browserName: "internet explorer",
        version: "9"
    },
    "sl_ie_10": {
        base: "SauceLabs",
        browserName: "internet explorer",
        version: "10"
    },
    "sl_ie_11": {
        base: "SauceLabs",
        browserName: "internet explorer",
        version: "11"
    }
};

module.exports = function(config) {

  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: "",

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ["browserify", "mocha", "chai"], // "detectBrowsers"],

    // list of files / patterns to load in the browser
    files: [
      "node_modules/babel/polyfill.js",
      "es5/**/*.js"
    ],

    // list of files to exclude
    exclude: [
    ],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
        "node_modules/babel/polyfill.js": ["browserify"],
        "es5/**/*.js": ["browserify"]
    },

    browserify: {
      debug: false
    },

    sauceLabs: {
        testName: "Jargon.js",
        recordVideo: true
    },

    customLaunchers: customLaunchers,

    // test results reporter to use
    // possible values: "dots", "progress"
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ["progress", "saucelabs"],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_DEBUG,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: false,

    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: Object.keys(customLaunchers), // "Chrome", "Firefox", "Safari", "Opera", "IE"],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false
  });
};
<commit_msg>Still trying to get saucelabs to work with Travis-CI<commit_after>var customLaunchers = {
    // Chrome
    "sl_chrome_43": {
        base: "SauceLabs",
        browserName: "chrome",
        version: "43"
    },
    "sl_chrome_42": {
        base: "SauceLabs",
        browserName: "chrome",
        version: "42"
    },
    "sl_chrome_36": {
        base: "SauceLabs",
        browserName: "chrome",
        version: "36"
    },
    "sl_chrome_31": {
        base: "SauceLabs",
        browserName: "chrome",
        version: "31"
    },

    // Firefox
    "sl_firefox_37": {
        base: "SauceLabs",
        browserName: "firefox",
        version: "37"
    },
    "sl_firefox_38": {
        base: "SauceLabs",
        browserName: "firefox",
        version: "38"
    },

    // Internet Explorer
    "sl_ie_9": {
        base: "SauceLabs",
        browserName: "internet explorer",
        version: "9"
    },
    "sl_ie_10": {
        base: "SauceLabs",
        browserName: "internet explorer",
        version: "10"
    },
    "sl_ie_11": {
        base: "SauceLabs",
        browserName: "internet explorer",
        version: "11"
    }
};

module.exports = function(config) {

  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: "",

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ["browserify", "mocha", "chai"], // "detectBrowsers"],

    // list of files / patterns to load in the browser
    files: [
      "node_modules/babel/polyfill.js",
      "es5/**/*.js"
    ],

    // list of files to exclude
    exclude: [
    ],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
        "node_modules/babel/polyfill.js": ["browserify"],
        "es5/**/*.js": ["browserify"]
    },

    browserify: {
      debug: false
    },

    sauceLabs: {
        testName: "Jargon.js",
        recordVideo: true,
        startConnect: false
    },

    customLaunchers: customLaunchers,

    // test results reporter to use
    // possible values: "dots", "progress"
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ["progress", "saucelabs"],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_DEBUG,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: false,

    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: Object.keys(customLaunchers), // "Chrome", "Firefox", "Safari", "Opera", "IE"],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false
  });
};
<|endoftext|>


<commit_before>(function(module) {
    'use strict';
    module.factory('mainMapService', [
        '$window', '$location', '$timeout', 'Leaflet', 'leafletData', 'backendService',
        function($window, $location, $timeout, L, leafletData, backendService) {
            /**
             * Default zoom used when no one provided a zoom level
             * @type {number}
             */
            var DEFAULT_ZOOM = 2;

            /**
             * Zoom level used when the backend provides the zoom level
             * based on the IP address
             * @type {number}
             */
            var GEO_IP_ZOOM = 10;

            /**
             * Local storage id used for storing the current map center
             * @type {string}
             */
            var MAP_CENTER_STORAGE_ID = 'veganautMapCenter';

            /**
             * How many digits after the decimal point should be considered for lat/lng
             * @type {number}
             */
            var FLOAT_PRECISION = 7;

            /**
             * Service managing the main map. This mostly concerns the
             * storage and retrieval of the map center from different sources.
             * @constructor
             */
            var MainMapService = function() {
                /**
                 * Current center of the map
                 * @type {{lat: number, lng: number, zoom: number}}
                 */
                this.center = {
                    lat: 0,
                    lng: 0,
                    zoom: DEFAULT_ZOOM
                };

                this._initialiseMapCenter();
            };

            /**
             * Sets the map center either from the url hash, local storage or
             * from asking the backend for a location
             * @private
             */
            MainMapService.prototype._initialiseMapCenter = function() {
                var that = this;

                // Try setting from the URL hash
                var centerSet = that.setMapCenterFromUrl();

                // Try to load from center from local storage
                if (!centerSet) {
                    centerSet = that._setMapCenterFromLocalStorage();
                }

                // Finally, ask the backend for a center
                if (!centerSet) {
                    backendService.getGeoIP().then(function(res) {
                        // Try to set the received center
                        that._setMapCenterIfValid(res.data.lat, res.data.lng, GEO_IP_ZOOM);
                    });
                }
            };

            /**
             * Checks if the given center and zoom coordinates are valid
             * and sets them if they are.
             * @param {number} lat
             * @param {number} lng
             * @param {number} zoom
             * @returns {boolean} Whether the center was set
             * @private
             */
            MainMapService.prototype._setMapCenterIfValid = function(lat, lng, zoom) {
                if (angular.isNumber(lat) && isFinite(lat) &&
                    angular.isNumber(lng) && isFinite(lat) &&
                    angular.isNumber(zoom) && isFinite(lat))
                {
                    // Only set new coords if either not defined or significantly enough different
                    if (!angular.isNumber(this.center.lat) ||
                        this.center.lat.toFixed(FLOAT_PRECISION) !== lat.toFixed(FLOAT_PRECISION))
                    {
                        this.center.lat = lat;
                    }
                    if (!angular.isNumber(this.center.lng) ||
                        this.center.lng.toFixed(FLOAT_PRECISION) !== lng.toFixed(FLOAT_PRECISION))
                    {
                        this.center.lng = lng;
                    }

                    // Set zoom
                    this.center.zoom = zoom;

                    this.saveCenter();
                    return true;
                }
                return false;
            };

            /**
             * Try to read the map center from local storage
             * @returns {boolean} Whether the center was set
             * @private
             */
            MainMapService.prototype._setMapCenterFromLocalStorage = function() {
                var center = JSON.parse($window.localStorage.getItem(MAP_CENTER_STORAGE_ID) || '{}');
                return this._setMapCenterIfValid(center.lat, center.lng, center.zoom);
            };

            /**
             * Try to read the map center from the url hash
             * @returns {boolean} Whether the center was set
             */
            MainMapService.prototype.setMapCenterFromUrl = function() {
                // Hash is in the form: zoom:11,coords:46.9767388-7.6516342
                var hashArgs = $location.hash().split(',');
                var zoom, lat, lng;

                // Go through all the arguments
                for (var i = 0; i < hashArgs.length; i++) {
                    var arg = hashArgs[i].split(':');
                    if (arg[0] === 'zoom') {
                        // Found zoom value
                        zoom = parseInt(arg[1], 10);
                    }
                    else if (arg[0] === 'coords') {
                        // Found coordinates
                        var coords = arg[1].split('-');
                        if (coords.length === 2) {
                            lat = parseFloat(coords[0]);
                            lng = parseFloat(coords[1]);
                        }
                    }
                }

                return this._setMapCenterIfValid(lat, lng, zoom);
            };

            /**
             * Saves the map center in local storage and in the url
             */
            MainMapService.prototype.saveCenter = function() {
                // Store it in local storage
                $window.localStorage.setItem(MAP_CENTER_STORAGE_ID,
                    JSON.stringify(this.center)
                );

                // Store it in the url hash (without adding a new history item)
                $location.replace();
                $location.hash(
                    'zoom:' +
                    this.center.zoom +
                    ',coords:' +
                    this.center.lat.toFixed(FLOAT_PRECISION) + '-' +
                    this.center.lng.toFixed(FLOAT_PRECISION)
                );
            };

            /**
             * Redirects to the location list that will show a locations within a radius from
             * a center that is picked from the current map view.
             * TODO: Add a wrapper for the leaflet map instance with helper methods.
             * TODO: add tests once this method is more split up
             * @param {number} headerSize Size of the header (under which the map is shown,
             *      but should not be considered for the radius and center calculation)
             */
            MainMapService.prototype.goToLocationList = function(headerSize) {
                leafletData.getMap().then(function(map) {
                    // Get the size of the map area in pixels
                    var containerSizePixel = map.getSize();

                    // Get the center around which we want to show the locations
                    // This is the point in the center of the visible part of the map
                    // (remember that the actual map goes under the header).
                    var adjustedCenter = map.containerPointToLatLng([containerSizePixel.x / 2, (containerSizePixel.y + headerSize) / 2]);

                    // Calculate the distance of the shown vertical space in meters
                    // It is possible that not the whole map container is used to show the map, if one is zoomed out
                    // very far. We want to get the most southern and most northern point on the map that is shown.
                    // First we find out the pixel coordinates of the poles
                    var northPole = L.latLng(90, 0);
                    var southPole = L.latLng(-90, 0);
                    var northPolePixel = map.latLngToContainerPoint(northPole);
                    var southPolePixel = map.latLngToContainerPoint(southPole);

                    // In most cases, the height of the map  is the height of the container (minus the header)
                    var mapHeightPixel = containerSizePixel.y - headerSize;

                    // The northern most point is either the north pole, or the point just at the header size
                    var northernMostPoint;
                    if (northPolePixel.y > headerSize) {
                        // The north pole is visible, set that as the northern most point
                        northernMostPoint = northPole;

                        // Above the north pole, there might be empty space that we don't want to count)
                        mapHeightPixel -= (northPolePixel.y - headerSize);
                    }
                    else {
                        // North pole is not visible, so northern most point is just at the header size
                        northernMostPoint = map.containerPointToLatLng([0, headerSize]);
                    }

                    // Same thing for the southern most point, except that we don't account for the header
                    var southernMostPoint;
                    if (southPolePixel.y < containerSizePixel.y) {
                        southernMostPoint = southPole;
                        mapHeightPixel -= (containerSizePixel.y - southPolePixel.y);
                    }
                    else {
                        southernMostPoint = map.containerPointToLatLng([0, containerSizePixel.y]);
                    }

                    // Finally, get the vertical distance (in meters) by taking the distance from north to south
                    var verticalDistance = northernMostPoint.distanceTo(southernMostPoint);

                    // If not the whole vertical space was used up by the map, scale up the distance
                    // to get how many meters would fit in the whole map area.
                    verticalDistance = (verticalDistance / mapHeightPixel) * (containerSizePixel.y - headerSize);

                    // For the horizontal distance, we use a different technique:
                    // We measure how many meters fit in 100 pixels at the "widest" part of the map.
                    // This has to be done because we cannot simply take the distance from the eastern most
                    // point to the western most point, because the shortest path between those might be
                    // going "the other way around" or because multiple "earths" might be shown.
                    // The widest part of the map is the latitude that is closest to the equator. We first
                    // find out what latitude this is and pick a point on it.
                    var horizontalPoint1;
                    if (northernMostPoint.lat < 0) {
                        // If the northern most point is below the equator, it's the closest to the equator
                        horizontalPoint1 = L.latLng(northernMostPoint.lat, 0);
                    }
                    else if (southernMostPoint.lat > 0) {
                        // If the southern most point is above the equator, it's the closest to the equator
                        horizontalPoint1 = L.latLng(southernMostPoint.lat, 0);
                    }
                    else {
                        // Otherwise, the equator itself is withing the view
                        horizontalPoint1 = L.latLng(0, 0);
                    }

                    // Convert to pixel coordinates
                    var horizontalPoint1Pixel = map.latLngToContainerPoint(horizontalPoint1);

                    // Get a second point, 100 pixels east of the first point
                    var horizontalPoint2 = map.containerPointToLatLng([horizontalPoint1Pixel.x + 100, horizontalPoint1Pixel.y]);

                    // Take the distance between these two points and scale up to the whole container size
                    var horizontalDistance = (horizontalPoint1.distanceTo(horizontalPoint2) / 100) * containerSizePixel.x;

                    // Finally, we can pick a good radius by choosing the smaller of the two distances
                    var radius = Math.min(horizontalDistance, verticalDistance) / 2;

                    // Show the rough area that was selected on the map.
                    // We draw the biggest circle that fully fits in the current map view.
                    // This is done in pixel-coordinates since if we get close to the poles and are zoomed out
                    // quite far, the map coordinate circle are getting way too big.
                    // Note that the further we are zoomed out and the further we get to the poles, this
                    // approximation of drawing a circle on a mercator projection to show an area within a certain
                    // radius around a point of the sphere gets more and more imprecise.
                    // TODO: try to draw the actual projection of the circle
                    var circleMarker = L.circleMarker(adjustedCenter, {
                        className: 'geolocate-circle-marker'
                    });
                    var radiusPixel = Math.min(containerSizePixel.x, containerSizePixel.y - headerSize) / 2;
                    circleMarker.setRadius(radiusPixel);
                    circleMarker.addTo(map);

                    // Redirect after a bit
                    $timeout(function() {
                        // Remove the circle marker again
                        map.removeLayer(circleMarker);

                        // TODO: already start loading the locations now for the location list
                        $location
                            .path('list/locations')
                            .search('lat', adjustedCenter.lat.toFixed(FLOAT_PRECISION))
                            .search('lng', adjustedCenter.lng.toFixed(FLOAT_PRECISION))
                            .search('radius', radius.toFixed(0))
                        ;
                    }, 500);
                });
            };

            return new MainMapService();
        }
    ]);
})(window.veganaut.mapModule);
<commit_msg>Going directly to list/locations/ when visiting the list from the map (to prevent double $routeChangeSuccess).<commit_after>(function(module) {
    'use strict';
    module.factory('mainMapService', [
        '$window', '$location', '$timeout', 'Leaflet', 'leafletData', 'backendService',
        function($window, $location, $timeout, L, leafletData, backendService) {
            /**
             * Default zoom used when no one provided a zoom level
             * @type {number}
             */
            var DEFAULT_ZOOM = 2;

            /**
             * Zoom level used when the backend provides the zoom level
             * based on the IP address
             * @type {number}
             */
            var GEO_IP_ZOOM = 10;

            /**
             * Local storage id used for storing the current map center
             * @type {string}
             */
            var MAP_CENTER_STORAGE_ID = 'veganautMapCenter';

            /**
             * How many digits after the decimal point should be considered for lat/lng
             * @type {number}
             */
            var FLOAT_PRECISION = 7;

            /**
             * Service managing the main map. This mostly concerns the
             * storage and retrieval of the map center from different sources.
             * @constructor
             */
            var MainMapService = function() {
                /**
                 * Current center of the map
                 * @type {{lat: number, lng: number, zoom: number}}
                 */
                this.center = {
                    lat: 0,
                    lng: 0,
                    zoom: DEFAULT_ZOOM
                };

                this._initialiseMapCenter();
            };

            /**
             * Sets the map center either from the url hash, local storage or
             * from asking the backend for a location
             * @private
             */
            MainMapService.prototype._initialiseMapCenter = function() {
                var that = this;

                // Try setting from the URL hash
                var centerSet = that.setMapCenterFromUrl();

                // Try to load from center from local storage
                if (!centerSet) {
                    centerSet = that._setMapCenterFromLocalStorage();
                }

                // Finally, ask the backend for a center
                if (!centerSet) {
                    backendService.getGeoIP().then(function(res) {
                        // Try to set the received center
                        that._setMapCenterIfValid(res.data.lat, res.data.lng, GEO_IP_ZOOM);
                    });
                }
            };

            /**
             * Checks if the given center and zoom coordinates are valid
             * and sets them if they are.
             * @param {number} lat
             * @param {number} lng
             * @param {number} zoom
             * @returns {boolean} Whether the center was set
             * @private
             */
            MainMapService.prototype._setMapCenterIfValid = function(lat, lng, zoom) {
                if (angular.isNumber(lat) && isFinite(lat) &&
                    angular.isNumber(lng) && isFinite(lat) &&
                    angular.isNumber(zoom) && isFinite(lat))
                {
                    // Only set new coords if either not defined or significantly enough different
                    if (!angular.isNumber(this.center.lat) ||
                        this.center.lat.toFixed(FLOAT_PRECISION) !== lat.toFixed(FLOAT_PRECISION))
                    {
                        this.center.lat = lat;
                    }
                    if (!angular.isNumber(this.center.lng) ||
                        this.center.lng.toFixed(FLOAT_PRECISION) !== lng.toFixed(FLOAT_PRECISION))
                    {
                        this.center.lng = lng;
                    }

                    // Set zoom
                    this.center.zoom = zoom;

                    this.saveCenter();
                    return true;
                }
                return false;
            };

            /**
             * Try to read the map center from local storage
             * @returns {boolean} Whether the center was set
             * @private
             */
            MainMapService.prototype._setMapCenterFromLocalStorage = function() {
                var center = JSON.parse($window.localStorage.getItem(MAP_CENTER_STORAGE_ID) || '{}');
                return this._setMapCenterIfValid(center.lat, center.lng, center.zoom);
            };

            /**
             * Try to read the map center from the url hash
             * @returns {boolean} Whether the center was set
             */
            MainMapService.prototype.setMapCenterFromUrl = function() {
                // Hash is in the form: zoom:11,coords:46.9767388-7.6516342
                var hashArgs = $location.hash().split(',');
                var zoom, lat, lng;

                // Go through all the arguments
                for (var i = 0; i < hashArgs.length; i++) {
                    var arg = hashArgs[i].split(':');
                    if (arg[0] === 'zoom') {
                        // Found zoom value
                        zoom = parseInt(arg[1], 10);
                    }
                    else if (arg[0] === 'coords') {
                        // Found coordinates
                        var coords = arg[1].split('-');
                        if (coords.length === 2) {
                            lat = parseFloat(coords[0]);
                            lng = parseFloat(coords[1]);
                        }
                    }
                }

                return this._setMapCenterIfValid(lat, lng, zoom);
            };

            /**
             * Saves the map center in local storage and in the url
             */
            MainMapService.prototype.saveCenter = function() {
                // Store it in local storage
                $window.localStorage.setItem(MAP_CENTER_STORAGE_ID,
                    JSON.stringify(this.center)
                );

                // Store it in the url hash (without adding a new history item)
                $location.replace();
                $location.hash(
                    'zoom:' +
                    this.center.zoom +
                    ',coords:' +
                    this.center.lat.toFixed(FLOAT_PRECISION) + '-' +
                    this.center.lng.toFixed(FLOAT_PRECISION)
                );
            };

            /**
             * Redirects to the location list that will show a locations within a radius from
             * a center that is picked from the current map view.
             * TODO: Add a wrapper for the leaflet map instance with helper methods.
             * TODO: add tests once this method is more split up
             * @param {number} headerSize Size of the header (under which the map is shown,
             *      but should not be considered for the radius and center calculation)
             */
            MainMapService.prototype.goToLocationList = function(headerSize) {
                leafletData.getMap().then(function(map) {
                    // Get the size of the map area in pixels
                    var containerSizePixel = map.getSize();

                    // Get the center around which we want to show the locations
                    // This is the point in the center of the visible part of the map
                    // (remember that the actual map goes under the header).
                    var adjustedCenter = map.containerPointToLatLng([containerSizePixel.x / 2, (containerSizePixel.y + headerSize) / 2]);

                    // Calculate the distance of the shown vertical space in meters
                    // It is possible that not the whole map container is used to show the map, if one is zoomed out
                    // very far. We want to get the most southern and most northern point on the map that is shown.
                    // First we find out the pixel coordinates of the poles
                    var northPole = L.latLng(90, 0);
                    var southPole = L.latLng(-90, 0);
                    var northPolePixel = map.latLngToContainerPoint(northPole);
                    var southPolePixel = map.latLngToContainerPoint(southPole);

                    // In most cases, the height of the map  is the height of the container (minus the header)
                    var mapHeightPixel = containerSizePixel.y - headerSize;

                    // The northern most point is either the north pole, or the point just at the header size
                    var northernMostPoint;
                    if (northPolePixel.y > headerSize) {
                        // The north pole is visible, set that as the northern most point
                        northernMostPoint = northPole;

                        // Above the north pole, there might be empty space that we don't want to count)
                        mapHeightPixel -= (northPolePixel.y - headerSize);
                    }
                    else {
                        // North pole is not visible, so northern most point is just at the header size
                        northernMostPoint = map.containerPointToLatLng([0, headerSize]);
                    }

                    // Same thing for the southern most point, except that we don't account for the header
                    var southernMostPoint;
                    if (southPolePixel.y < containerSizePixel.y) {
                        southernMostPoint = southPole;
                        mapHeightPixel -= (containerSizePixel.y - southPolePixel.y);
                    }
                    else {
                        southernMostPoint = map.containerPointToLatLng([0, containerSizePixel.y]);
                    }

                    // Finally, get the vertical distance (in meters) by taking the distance from north to south
                    var verticalDistance = northernMostPoint.distanceTo(southernMostPoint);

                    // If not the whole vertical space was used up by the map, scale up the distance
                    // to get how many meters would fit in the whole map area.
                    verticalDistance = (verticalDistance / mapHeightPixel) * (containerSizePixel.y - headerSize);

                    // For the horizontal distance, we use a different technique:
                    // We measure how many meters fit in 100 pixels at the "widest" part of the map.
                    // This has to be done because we cannot simply take the distance from the eastern most
                    // point to the western most point, because the shortest path between those might be
                    // going "the other way around" or because multiple "earths" might be shown.
                    // The widest part of the map is the latitude that is closest to the equator. We first
                    // find out what latitude this is and pick a point on it.
                    var horizontalPoint1;
                    if (northernMostPoint.lat < 0) {
                        // If the northern most point is below the equator, it's the closest to the equator
                        horizontalPoint1 = L.latLng(northernMostPoint.lat, 0);
                    }
                    else if (southernMostPoint.lat > 0) {
                        // If the southern most point is above the equator, it's the closest to the equator
                        horizontalPoint1 = L.latLng(southernMostPoint.lat, 0);
                    }
                    else {
                        // Otherwise, the equator itself is withing the view
                        horizontalPoint1 = L.latLng(0, 0);
                    }

                    // Convert to pixel coordinates
                    var horizontalPoint1Pixel = map.latLngToContainerPoint(horizontalPoint1);

                    // Get a second point, 100 pixels east of the first point
                    var horizontalPoint2 = map.containerPointToLatLng([horizontalPoint1Pixel.x + 100, horizontalPoint1Pixel.y]);

                    // Take the distance between these two points and scale up to the whole container size
                    var horizontalDistance = (horizontalPoint1.distanceTo(horizontalPoint2) / 100) * containerSizePixel.x;

                    // Finally, we can pick a good radius by choosing the smaller of the two distances
                    var radius = Math.min(horizontalDistance, verticalDistance) / 2;

                    // Show the rough area that was selected on the map.
                    // We draw the biggest circle that fully fits in the current map view.
                    // This is done in pixel-coordinates since if we get close to the poles and are zoomed out
                    // quite far, the map coordinate circle are getting way too big.
                    // Note that the further we are zoomed out and the further we get to the poles, this
                    // approximation of drawing a circle on a mercator projection to show an area within a certain
                    // radius around a point of the sphere gets more and more imprecise.
                    // TODO: try to draw the actual projection of the circle
                    var circleMarker = L.circleMarker(adjustedCenter, {
                        className: 'geolocate-circle-marker'
                    });
                    var radiusPixel = Math.min(containerSizePixel.x, containerSizePixel.y - headerSize) / 2;
                    circleMarker.setRadius(radiusPixel);
                    circleMarker.addTo(map);

                    // Redirect after a bit
                    $timeout(function() {
                        // Remove the circle marker again
                        map.removeLayer(circleMarker);

                        // TODO: already start loading the locations now for the location list
                        $location
                            .path('list/locations/')
                            .search('lat', adjustedCenter.lat.toFixed(FLOAT_PRECISION))
                            .search('lng', adjustedCenter.lng.toFixed(FLOAT_PRECISION))
                            .search('radius', radius.toFixed(0))
                        ;
                    }, 500);
                });
            };

            return new MainMapService();
        }
    ]);
})(window.veganaut.mapModule);
<|endoftext|>


<commit_before>describe('dialogUserController', function() {
  var $controller, API, dialogFieldRefreshService, dialogUserSubmitErrorHandlerService, miqService;

  beforeEach(module('ManageIQ'));

  beforeEach(inject(function(_$controller_,
                             _API_,
                             _dialogFieldRefreshService_,
                             _dialogUserSubmitErrorHandlerService_,
                             _miqService_) {
    API = _API_;
    dialogFieldRefreshService = _dialogFieldRefreshService_;
    dialogUserSubmitErrorHandlerService = _dialogUserSubmitErrorHandlerService_;
    miqService = _miqService_;

    var responseResult = {content: ['the dialog']};

    spyOn(API, 'get').and.callFake(function() {
      return {then: function(response) { response(responseResult); }};
    });

    spyOn(dialogFieldRefreshService, 'refreshField');
    spyOn(dialogUserSubmitErrorHandlerService, 'handleError');
    spyOn(miqService, 'miqAjaxButton');
    spyOn(miqService, 'redirectBack');
    spyOn(miqService, 'sparkleOn');
    spyOn(miqService, 'sparkleOff');
    spyOn(miqService, 'refreshSelectpicker');

    $controller = _$controller_('dialogUserController', {
      API: API,
      dialogFieldRefreshService: dialogFieldRefreshService,
      dialogUserSubmitErrorHandlerService: dialogUserSubmitErrorHandlerService,
      miqService: miqService,
      dialogId: '1234',
      apiSubmitEndpoint: 'submit endpoint',
      apiAction: 'order',
      cancelEndpoint: 'cancel endpoint',
      finishSubmitEndpoint: 'finish submit endpoint',
      resourceActionId: '789',
      targetId: '987',
      targetType: 'targettype',
    });
  }));

  describe('$onInit', function() {
    beforeEach(function() {
      $controller.$onInit();
    });

    it('requests the current dialog based on the service template', function() {
      expect(API.get).toHaveBeenCalledWith(
        '/api/service_dialogs/1234?resource_action_id=789&target_id=987&target_type=targettype',
        {expand: 'resources', attributes: 'content'}
      );
    });

    it('resolves the request and stores the information in the dialog property', function() {
      expect($controller.dialog).toEqual('the dialog');
    });

    it('sets the refreshUrl', function() {
      expect($controller.refreshUrl).toEqual('/api/service_dialogs/');
    });

    it('sets dialogLoaded to true', function() {
      expect($controller.dialogLoaded).toEqual(true);
    });

    it('refreshes the selectpickers after resolving the api call', function(done) {
      setTimeout(function() {
        expect(miqService.refreshSelectpicker).toHaveBeenCalled();
        done();
      });
    });
  });

  describe('refreshField', function() {
    it('delegates to the dialogFieldRefreshService', function() {
      $controller.dialogData = 'dialogData';

      $controller.refreshField({name: 'dialogName'});
      expect(dialogFieldRefreshService.refreshField).toHaveBeenCalledWith(
        'dialogData',
        ['dialogName'],
        '/api/service_dialogs/',
        {dialogId: '1234', resourceActionId: '789', targetId: '987', targetType: 'targettype'}
      );
    });
  });

  describe('setDialogData', function() {
    it('sets the data attribute of the data passed in to dialogData', function() {
      expect($controller.dialogData).toEqual(undefined);

      $controller.setDialogData({data: 'newData', validations: { isValid : true}});

      expect($controller.dialogData).toEqual('newData');
    });
  });

  describe('submitButtonClicked', function() {
    beforeEach(function() {
      $controller.setDialogData({data: {field1: 'field1'}, validations: { isValid : true}});
    });

    context('when the submit endpoint deals with generic objects', function() {
      beforeEach(inject(function(_$controller_) {
        $controller = _$controller_('dialogUserController', {
          API: API,
          dialogFieldRefreshService: dialogFieldRefreshService,
          miqService: miqService,
          dialogId: '1234',
          apiSubmitEndpoint: 'generic_objects/explorer',
          apiAction: 'custom_action',
          cancelEndpoint: 'cancel endpoint',
          finishSubmitEndpoint: 'finish submit endpoint',
          resourceActionId: '789',
          targetId: '987',
          targetType: 'targettype',
          saveable: true,
        });

        $controller.setDialogData({data: {field1: 'field1'}, validations: { isValid : true}});

        spyOn(API, 'post').and.returnValue(Promise.resolve('awesome'));
      }));

      it('posts to the API with the right data', function(done) {
        $controller.submitButtonClicked();

        setTimeout(function() {
          expect(API.post).toHaveBeenCalledWith('generic_objects/explorer', {
            parameters: {field1: 'field1'}, action: 'custom_action'}, {skipErrors: [400]});
          done();
        });
      });
    });

    context('when the api action is reconfigure', function() {
      beforeEach(inject(function(_$controller_) {
        $controller = _$controller_('dialogUserController', {
          API: API,
          dialogFieldRefreshService: dialogFieldRefreshService,
          miqService: miqService,
          dialogId: '1234',
          apiSubmitEndpoint: 'service/explorer',
          apiAction: 'reconfigure',
          cancelEndpoint: 'cancel endpoint',
          finishSubmitEndpoint: 'finish submit endpoint',
          resourceActionId: '789',
          targetId: '987',
          targetType: 'targettype',
          saveable: true,
        });

        $controller.setDialogData({data: {field1: 'field1'}, validations: { isValid : true}});

        spyOn(API, 'post').and.returnValue(Promise.resolve('awesome'));
      }));

      it('posts to the API with the right data', function(done) {
        $controller.submitButtonClicked();

        setTimeout(function() {
          expect(API.post).toHaveBeenCalledWith('service/explorer', {
            resource: {field1: 'field1'}, action: 'reconfigure'}, {skipErrors: [400]});
          done();
        });
      });
    });

    context('when the API call succeeds', function() {
      beforeEach(function() {
        spyOn(API, 'post').and.returnValue(Promise.resolve('awesome'));
      });

      it('starts the sparkle', function(done) {
        $controller.submitButtonClicked();

        setTimeout(function() {
          expect(miqService.sparkleOn).toHaveBeenCalled();
          done();
        });
      });

      it('posts to the API using the correct url and data', function(done) {
        $controller.submitButtonClicked();

        setTimeout(function() {
          expect(API.post).toHaveBeenCalledWith('submit endpoint', {
            action: 'order',
            field1: 'field1'
          }, {skipErrors: [400]});
          done();
        });
      });

      it('redirects to the miq_request screen', function(done) {
        $controller.submitButtonClicked();
        setTimeout(function() {
          expect(miqService.redirectBack).toHaveBeenCalledWith(
            'Order Request was Submitted', 'info', 'finish submit endpoint'
          );
          done();
        });
      });
    });

    context('when the API call fails', function() {
      var rejectionData;

      beforeEach(function() {
        rejectionData = {data: {error: {message: "Failed! -One,Two"}}};
        spyOn(API, 'post').and.returnValue(Promise.reject(rejectionData));
      });

      it('delegates to the dialogUserSubmitErrorHandlerService', function(done) {
        $controller.submitButtonClicked()
          .catch(function() {
            expect(dialogUserSubmitErrorHandlerService.handleError).toHaveBeenCalledWith(rejectionData);
            done();
          });
      });
    });
  });

  describe('cancelClicked', function() {
    it('uses the miqService to make a call to catalog/explorer', function() {
      $controller.cancelClicked('event');
      expect(miqService.redirectBack).toHaveBeenCalledWith('Dialog Cancelled', 'info', 'cancel endpoint');
    });
  });

  describe('saveable', function() {
    context('when fields are being refreshed', function() {
      beforeEach(function() {
        dialogFieldRefreshService.areFieldsBeingRefreshed = true;
        $controller.isValid = true;
      });

      it('returns false', function() {
        expect($controller.saveable()).toBe(false);
      });
    });

    context('when fields are not being refreshed', function() {
      beforeEach(function() {
        dialogFieldRefreshService.areFieldsBeingRefreshed = false;
        $controller.isValid = true;
      });

      it('returns true', function() {
        expect($controller.saveable()).toBe(true);
      });
    });
  });
});
<commit_msg>Add openUrl set to false to tests as a default option<commit_after>describe('dialogUserController', function() {
  var $controller, API, dialogFieldRefreshService, dialogUserSubmitErrorHandlerService, miqService;

  beforeEach(module('ManageIQ'));

  beforeEach(inject(function(_$controller_,
                             _API_,
                             _dialogFieldRefreshService_,
                             _dialogUserSubmitErrorHandlerService_,
                             _miqService_) {
    API = _API_;
    dialogFieldRefreshService = _dialogFieldRefreshService_;
    dialogUserSubmitErrorHandlerService = _dialogUserSubmitErrorHandlerService_;
    miqService = _miqService_;

    var responseResult = {content: ['the dialog']};

    spyOn(API, 'get').and.callFake(function() {
      return {then: function(response) { response(responseResult); }};
    });

    spyOn(dialogFieldRefreshService, 'refreshField');
    spyOn(dialogUserSubmitErrorHandlerService, 'handleError');
    spyOn(miqService, 'miqAjaxButton');
    spyOn(miqService, 'redirectBack');
    spyOn(miqService, 'sparkleOn');
    spyOn(miqService, 'sparkleOff');
    spyOn(miqService, 'refreshSelectpicker');

    $controller = _$controller_('dialogUserController', {
      API: API,
      dialogFieldRefreshService: dialogFieldRefreshService,
      dialogUserSubmitErrorHandlerService: dialogUserSubmitErrorHandlerService,
      miqService: miqService,
      dialogId: '1234',
      apiSubmitEndpoint: 'submit endpoint',
      apiAction: 'order',
      cancelEndpoint: 'cancel endpoint',
      finishSubmitEndpoint: 'finish submit endpoint',
      resourceActionId: '789',
      targetId: '987',
      targetType: 'targettype',
      openUrl: false,
    });
  }));

  describe('$onInit', function() {
    beforeEach(function() {
      $controller.$onInit();
    });

    it('requests the current dialog based on the service template', function() {
      expect(API.get).toHaveBeenCalledWith(
        '/api/service_dialogs/1234?resource_action_id=789&target_id=987&target_type=targettype',
        {expand: 'resources', attributes: 'content'}
      );
    });

    it('resolves the request and stores the information in the dialog property', function() {
      expect($controller.dialog).toEqual('the dialog');
    });

    it('sets the refreshUrl', function() {
      expect($controller.refreshUrl).toEqual('/api/service_dialogs/');
    });

    it('sets dialogLoaded to true', function() {
      expect($controller.dialogLoaded).toEqual(true);
    });

    it('refreshes the selectpickers after resolving the api call', function(done) {
      setTimeout(function() {
        expect(miqService.refreshSelectpicker).toHaveBeenCalled();
        done();
      });
    });
  });

  describe('refreshField', function() {
    it('delegates to the dialogFieldRefreshService', function() {
      $controller.dialogData = 'dialogData';

      $controller.refreshField({name: 'dialogName'});
      expect(dialogFieldRefreshService.refreshField).toHaveBeenCalledWith(
        'dialogData',
        ['dialogName'],
        '/api/service_dialogs/',
        {dialogId: '1234', resourceActionId: '789', targetId: '987', targetType: 'targettype'}
      );
    });
  });

  describe('setDialogData', function() {
    it('sets the data attribute of the data passed in to dialogData', function() {
      expect($controller.dialogData).toEqual(undefined);

      $controller.setDialogData({data: 'newData', validations: { isValid : true}});

      expect($controller.dialogData).toEqual('newData');
    });
  });

  describe('submitButtonClicked', function() {
    beforeEach(function() {
      $controller.setDialogData({data: {field1: 'field1'}, validations: { isValid : true}});
    });

    context('when the submit endpoint deals with generic objects', function() {
      beforeEach(inject(function(_$controller_) {
        $controller = _$controller_('dialogUserController', {
          API: API,
          dialogFieldRefreshService: dialogFieldRefreshService,
          miqService: miqService,
          dialogId: '1234',
          apiSubmitEndpoint: 'generic_objects/explorer',
          apiAction: 'custom_action',
          cancelEndpoint: 'cancel endpoint',
          finishSubmitEndpoint: 'finish submit endpoint',
          resourceActionId: '789',
          targetId: '987',
          targetType: 'targettype',
          saveable: true,
          openUrl: false,
        });

        $controller.setDialogData({data: {field1: 'field1'}, validations: { isValid : true}});

        spyOn(API, 'post').and.returnValue(Promise.resolve('awesome'));
      }));

      it('posts to the API with the right data', function(done) {
        $controller.submitButtonClicked();

        setTimeout(function() {
          expect(API.post).toHaveBeenCalledWith('generic_objects/explorer', {
            parameters: {field1: 'field1'}, action: 'custom_action'}, {skipErrors: [400]});
          done();
        });
      });
    });

    context('when the api action is reconfigure', function() {
      beforeEach(inject(function(_$controller_) {
        $controller = _$controller_('dialogUserController', {
          API: API,
          dialogFieldRefreshService: dialogFieldRefreshService,
          miqService: miqService,
          dialogId: '1234',
          apiSubmitEndpoint: 'service/explorer',
          apiAction: 'reconfigure',
          cancelEndpoint: 'cancel endpoint',
          finishSubmitEndpoint: 'finish submit endpoint',
          resourceActionId: '789',
          targetId: '987',
          targetType: 'targettype',
          saveable: true,
          openUrl: false,
        });

        $controller.setDialogData({data: {field1: 'field1'}, validations: { isValid : true}});

        spyOn(API, 'post').and.returnValue(Promise.resolve('awesome'));
      }));

      it('posts to the API with the right data', function(done) {
        $controller.submitButtonClicked();

        setTimeout(function() {
          expect(API.post).toHaveBeenCalledWith('service/explorer', {
            resource: {field1: 'field1'}, action: 'reconfigure'}, {skipErrors: [400]});
          done();
        });
      });
    });

    context('when the API call succeeds', function() {
      beforeEach(function() {
        spyOn(API, 'post').and.returnValue(Promise.resolve('awesome'));
      });

      it('starts the sparkle', function(done) {
        $controller.submitButtonClicked();

        setTimeout(function() {
          expect(miqService.sparkleOn).toHaveBeenCalled();
          done();
        });
      });

      it('posts to the API using the correct url and data', function(done) {
        $controller.submitButtonClicked();

        setTimeout(function() {
          expect(API.post).toHaveBeenCalledWith('submit endpoint', {
            action: 'order',
            field1: 'field1'
          }, {skipErrors: [400]});
          done();
        });
      });

      it('redirects to the miq_request screen', function(done) {
        $controller.submitButtonClicked();
        setTimeout(function() {
          expect(miqService.redirectBack).toHaveBeenCalledWith(
            'Order Request was Submitted', 'info', 'finish submit endpoint'
          );
          done();
        });
      });
    });

    context('when the API call fails', function() {
      var rejectionData;

      beforeEach(function() {
        rejectionData = {data: {error: {message: "Failed! -One,Two"}}};
        spyOn(API, 'post').and.returnValue(Promise.reject(rejectionData));
      });

      it('delegates to the dialogUserSubmitErrorHandlerService', function(done) {
        $controller.submitButtonClicked()
          .catch(function() {
            expect(dialogUserSubmitErrorHandlerService.handleError).toHaveBeenCalledWith(rejectionData);
            done();
          });
      });
    });
  });

  describe('cancelClicked', function() {
    it('uses the miqService to make a call to catalog/explorer', function() {
      $controller.cancelClicked('event');
      expect(miqService.redirectBack).toHaveBeenCalledWith('Dialog Cancelled', 'info', 'cancel endpoint');
    });
  });

  describe('saveable', function() {
    context('when fields are being refreshed', function() {
      beforeEach(function() {
        dialogFieldRefreshService.areFieldsBeingRefreshed = true;
        $controller.isValid = true;
      });

      it('returns false', function() {
        expect($controller.saveable()).toBe(false);
      });
    });

    context('when fields are not being refreshed', function() {
      beforeEach(function() {
        dialogFieldRefreshService.areFieldsBeingRefreshed = false;
        $controller.isValid = true;
      });

      it('returns true', function() {
        expect($controller.saveable()).toBe(true);
      });
    });
  });
});
<|endoftext|>