diff --git a/src/controllers/staging.controller.js b/src/controllers/staging.controller.js index e1038d8a..69d8da11 100644 --- a/src/controllers/staging.controller.js +++ b/src/controllers/staging.controller.js @@ -71,6 +71,25 @@ export const findAll = async (req, res) => { } }; +export const generateOfferFile = async (req, res) => { + try { + await assertIfReadOnlyMode(); + await assertStagingTableNotEmpty(); + await assertHomeOrgExists(); + await assertWalletIsSynced(); + await assertNoPendingCommits(); + + const offerFile = await Staging.generateOfferFile(); + res.json(offerFile); + } catch (error) { + console.trace(error); + res.status(400).json({ + message: 'Error generating offer file.', + error: error.message, + }); + } +}; + export const commit = async (req, res) => { try { await assertIfReadOnlyMode(); diff --git a/src/datalayer/persistance.js b/src/datalayer/persistance.js index 03eec528..7ede30a2 100644 --- a/src/datalayer/persistance.js +++ b/src/datalayer/persistance.js @@ -437,4 +437,28 @@ const getMirrors = async (storeId) => { } }; +export const makeOffer = async (offer) => { + const options = { + url: `${rpcUrl}/make_offer `, + body: JSON.stringify(offer), + }; + + try { + const response = await request( + Object.assign({}, getBaseOptions(), options), + ); + + const data = JSON.parse(response); + + if (data.success) { + return data; + } + + throw new Error(data.error); + } catch (error) { + console.log(error); + throw error; + } +}; + export const addMirror = _addMirror; diff --git a/src/models/staging/staging.model.js b/src/models/staging/staging.model.js index a28a3411..2c815c4e 100644 --- a/src/models/staging/staging.model.js +++ b/src/models/staging/staging.model.js @@ -2,18 +2,26 @@ import _ from 'lodash'; import Sequelize from 'sequelize'; +const Op = Sequelize.Op; + const { Model } = Sequelize; import { Project, Unit, Organization, Issuance } from '../../models'; -import { encodeHex } from '../../utils/datalayer-utils'; +import { encodeHex, generateOffer } from '../../utils/datalayer-utils'; import * as rxjs from 'rxjs'; import { sequelize } from '../../database'; import datalayer from '../../datalayer'; +import { makeOffer } from '../../datalayer/persistance'; import ModelTypes from './staging.modeltypes.cjs'; import { formatModelAssociationName } from '../../utils/model-utils.js'; +import { + createXlsFromSequelizeResults, + transformFullXslsToChangeList, +} from '../../utils/xls'; + class Staging extends Model { static changes = new rxjs.Subject(); @@ -32,6 +40,174 @@ class Staging extends Model { return super.upsert(values, options); } + static generateOfferFile = async () => { + const stagingRecord = await Staging.findOne({ + // where: { isTransfer: true }, + where: { commited: false }, + raw: true, + }); + + const takerProjectRecord = _.head(JSON.parse(stagingRecord.data)); + + const myOrganization = await Organization.findOne({ + where: { isHome: true }, + raw: true, + }); + + const maker = { inclusions: [] }; + const taker = { inclusions: [] }; + + // The record still has the orgUid of the takerProjectRecord, + // we will update this to the correct orgUId later + maker.storeId = takerProjectRecord.orgUid; + taker.storeId = myOrganization.orgUid; + + const makerProjectRecord = await Project.findOne({ + where: { warehouseProjectId: takerProjectRecord.warehouseProjectId }, + include: Project.getAssociatedModels().map((association) => { + return { + model: association.model, + as: formatModelAssociationName(association), + }; + }), + }); + + makerProjectRecord.projectStatus = 'Transitioned'; + + const issuanceIds = makerProjectRecord.issuances.reduce((ids, issuance) => { + if (!ids.includes(issuance.id)) { + ids.push(issuance.id); + } + return ids; + }, []); + + let unitMakerRecords = await Unit.findAll({ + where: { + issuanceId: { [Op.in]: issuanceIds }, + }, + raw: true, + }); + + // Takers get an unlatered copy of all the project units from the maker + const unitTakerRecords = _.cloneDeep(unitMakerRecords); + + unitMakerRecords = unitMakerRecords.map((record) => { + record.unitStatus = 'Exported'; + return record; + }); + + const primaryProjectKeyMap = { + project: 'warehouseProjectId', + projectLocations: 'id', + labels: 'id', + issuances: 'id', + coBenefits: 'id', + relatedProjects: 'id', + estimations: 'id', + projectRatings: 'id', + }; + + const primaryUnitKeyMap = { + unit: 'warehouseUnitId', + labels: 'id', + label_units: 'id', + issuances: 'id', + }; + + const makerProjectXslsSheets = createXlsFromSequelizeResults({ + rows: [makerProjectRecord], + model: Project, + toStructuredCsv: true, + }); + + const takerProjectXslsSheets = createXlsFromSequelizeResults({ + rows: [takerProjectRecord], + model: Project, + toStructuredCsv: true, + }); + + const makerUnitXslsSheets = createXlsFromSequelizeResults({ + rows: unitMakerRecords, + model: Unit, + toStructuredCsv: true, + }); + + const takerUnitXslsSheets = createXlsFromSequelizeResults({ + rows: unitTakerRecords, + model: Unit, + toStructuredCsv: true, + }); + + const takerProjectInclusions = await transformFullXslsToChangeList( + takerProjectXslsSheets, + 'insert', + primaryProjectKeyMap, + ); + + const makerProjectInclusions = await transformFullXslsToChangeList( + makerProjectXslsSheets, + 'insert', + primaryProjectKeyMap, + ); + + const makerUnitInclusions = await transformFullXslsToChangeList( + makerUnitXslsSheets, + 'insert', + primaryUnitKeyMap, + ); + + const takerUnitInclusions = await transformFullXslsToChangeList( + takerUnitXslsSheets, + 'insert', + primaryUnitKeyMap, + ); + + /* Object.keys(maker.inclusions).forEach((table) => { + maker.inclusions[table] = maker.inclusions[table] + .filter((inclusion) => inclusion.action !== 'delete') + .map((inclusion) => ({ key: inclusion.key, value: inclusion.value })); + });*/ + + maker.inclusions.push( + ...makerProjectInclusions.project + .filter((inclusion) => inclusion.action !== 'delete') + .map((inclusion) => ({ + key: inclusion.key, + value: inclusion.value, + })), + ); + + maker.inclusions.push( + ...makerUnitInclusions.unit + .filter((inclusion) => inclusion.action !== 'delete') + .map((inclusion) => ({ + key: inclusion.key, + value: inclusion.value, + })), + ); + + taker.inclusions.push( + ...takerProjectInclusions.project + .filter((inclusion) => inclusion.action !== 'delete') + .map((inclusion) => ({ + key: inclusion.key, + value: inclusion.value, + })), + ); + + taker.inclusions.push( + ...takerUnitInclusions.unit + .filter((inclusion) => inclusion.action !== 'delete') + .map((inclusion) => ({ + key: inclusion.key, + value: inclusion.value, + })), + ); + + const offer = generateOffer(maker, taker); + return makeOffer(offer); + }; + // If the record was commited but the diff.original is null // that means that the original record no longer exists and // the staging record should be cleaned up. diff --git a/src/routes/v1/resources/staging.js b/src/routes/v1/resources/staging.js index b17a08c1..9e3cbc52 100644 --- a/src/routes/v1/resources/staging.js +++ b/src/routes/v1/resources/staging.js @@ -18,6 +18,10 @@ StagingRouter.get('/', validator.query(stagingGetQuerySchema), (req, res) => { return StagingController.findAll(req, res); }); +StagingRouter.get('/offer', (req, res) => { + return StagingController.generateOfferFile(req, res); +}); + StagingRouter.put('/', (req, res) => { return StagingController.editRecord(req, res); }); diff --git a/src/utils/datalayer-utils.js b/src/utils/datalayer-utils.js index 90df64f8..dbd30b55 100644 --- a/src/utils/datalayer-utils.js +++ b/src/utils/datalayer-utils.js @@ -31,3 +31,21 @@ export const keyValueToChangeList = (key, value, includeDelete) => { return changeList; }; + +export const generateOffer = (maker, taker) => { + return { + maker: [ + { + store_id: maker.storeId, + inclusions: maker.inclusions, + }, + ], + taker: [ + { + store_id: taker.storeId, + inclusions: taker.inclusions, + }, + ], + fee: 0, + }; +};