Skip to content
This repository has been archived by the owner on Mar 28, 2023. It is now read-only.

Commit

Permalink
Merge pull request #529 from OpenBazaar/order-details-dispute
Browse files Browse the repository at this point in the history
Order details dispute
  • Loading branch information
jjeffryes authored Jul 6, 2017
2 parents 873e839 + c6264c5 commit da72b26
Show file tree
Hide file tree
Showing 54 changed files with 2,034 additions and 360 deletions.
81 changes: 74 additions & 7 deletions js/languages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -603,8 +603,7 @@
"formats": {
"PHYSICAL_GOOD": "Physical Good",
"DIGITAL_GOOD": "Digital Good",
"SERVICE": "Service",
"CROWD_FUND": "Crowd fund"
"SERVICE": "Service"
},
"conditionTypes": {
"NEW": "New",
Expand Down Expand Up @@ -1073,7 +1072,7 @@
"noteFromLabel": "Note from %{store}:",
"copyLink": "Copy",
"digitalReadyForDlHeading": "Digital files are ready for download!",
"digitalReadyForDlText": "The files have been delivered. DOwnload when you're ready.",
"digitalReadyForDlText": "The files have been delivered. Download when you're ready.",
"urlLabel": "File URL",
"passwordLabel": "Password"
},
Expand All @@ -1095,6 +1094,39 @@
"heading": "Order Complete",
"reviewLabel": "%{name}'s Review:"
},
"disputeStarted": {
"heading": "Dispute Started",
"partyIsDisputing": "%{name} is disputing the order:",
"resolveBtn": "Resolve Dispute",
"genericIsDisputed": "The order is being disputed:",
"noReasonProvided": "No reason was provided."
},
"disputePayout": {
"heading": "Dispute Payout",
"buyerHeading": "Buyer",
"buyerHeadingWithName": "%{name} (buyer)",
"vendorHeading": "Vendor",
"vendorHeadingWithName": "%{name} (vendor)",
"moderatorHeading": "Moderator",
"moderatorHeadingWithName": "%{name} (moderator)",
"noteFromHeading": "Note from moderator:",
"noteFromHeadingWithName": "Note from %{name}:",
"btnAcceptPayout": "Accept Payout",
"acceptPayoutConfirm": {
"title": "Are you sure?",
"body": "Once accepted, the payout will process immediately",
"btnCancel": "Cancel",
"btnConfirm": "Yes, Accept"
}
},
"disputeAcceptance": {
"heading": "Dispute Closed",
"genericBuyerAcceptedPayout": "The buyer accepted the dispute payout",
"genericVendorAcceptedPayout": "The vendor accepted the dispute payout",
"userAcceptedPayout": "%{name} accepted the dispute payout",
"orderCompleteWhenYouReview": "The order will be complete when you leave a review.",
"orderCompleteWhenBuyerReviews": "The order will be complete when the buyer leaves a review."
},
"orderDetails": {
"progressBarStates": {
"paid": "Paid",
Expand All @@ -1116,9 +1148,6 @@
"moderatorHeading": "Moderator",
"copyAddress": "Copy",
"viewOnMap": "View on map"
},
"payForOrder": {
"heading": "Pay for your order"
}
},
"fulfillOrderTab": {
Expand All @@ -1136,6 +1165,33 @@
"noteHelperTextDigital": "The buyer will receive a notification containing the file information",
"btnCancel": "Cancel",
"btnSubmit": "Submit"
},
"resolveDisputeTab": {
"heading": "Resolve Dispute",
"buyerAmountLabel": "Buyer Amount",
"vendorAmountLabel": "Vendor Amount",
"commentLabel": "Comment",
"commentPlaceholder": "Explain your decision...",
"btnCancel": "Cancel",
"btnSubmit": "Submit",
"resolveConfirm": {
"title": "Are you sure?",
"body": "Please double check everything looks good",
"btnCancel": "Cancel",
"btnSubmit": "Submit"
}
},
"disputeOrderTab": {
"heading": "Dispute Order",
"moderatorLabel": "Moderator",
"reasonLabel": "Reason",
"reasonPlaceholder": "Please explain your reason for opening a dispute…",
"reasonHelperText": "Opening a dispute will notify and invite the moderator into the order to help resolve any conflicts",
"btnCancel": "Cancel",
"btnSubmit": "Submit"
},
"actionBar": {
"disputeOrderBtn": "Dispute Order"
}
},
"orderUtil": {
Expand All @@ -1144,7 +1200,10 @@
"failedCancelHeading": "There was an error canceling the order.",
"failedFulfillHeading": "There was an error fulfilling the order.",
"failedRefundHeading": "There was an error refunding the order.",
"failedCompleteHeading": "There was an error completing the order."
"failedCompleteHeading": "There was an error completing the order.",
"failedOpenDisputeHeading": "There was an error completing the order.",
"failedResolveHeading": "There was an error resolving the order.",
"failedAcceptPayoutHeading": "There was an error accepting the payout."
},
"exchangeRatesSyncer": {
"fetchingRatesStatusMsg": "Fetching exchange rates…",
Expand Down Expand Up @@ -1304,6 +1363,14 @@
"provideReview": "Please provide a review.",
"provideRating": "Please select a rating."
},
"resolveDisputeModelErrors": {
"provideAmount": "Please provide an amount.",
"percentageOutOfRange": "The amount must be between 0 and 100.",
"providePercentageAsNumber": "Please provide a vendor amount as a number.",
"totalPercentageOutOfRange": "The sum of the buyer and vendor amounts cannot exceed 100.",
"totalPercentageTooLow": "The sum of the buyer and vendor amounts must add up to 100.",
"provideResolution": "Please explain your decision."
},
"bitcoinCurrencyUnits": {
"BTC": "BTC",
"MBTC": "mBTC",
Expand Down
1 change: 0 additions & 1 deletion js/models/listing/Metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export default class extends BaseModel {
'PHYSICAL_GOOD',
'DIGITAL_GOOD',
'SERVICE',
'CROWD_FUND',
];
}

Expand Down
32 changes: 32 additions & 0 deletions js/models/order/Case.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,38 @@ export default class extends BaseModel {
// convert price fields
response.vendorContract.buyerOrder.payment.amount =
integerToDecimal(response.vendorContract.buyerOrder.payment.amount, true);

if (response.resolution) {
response.resolution.payout.buyerOutput =
response.resolution.payout.buyerOutput || {};
response.resolution.payout.vendorOutput =
response.resolution.payout.vendorOutput || {};
response.resolution.payout.moderatorOutput =
response.resolution.payout.moderatorOutput || {};

// Temporary to account for server bug:
// https://github.com/OpenBazaar/openbazaar-go/issues/548
// Sometimes the payment amounts are coming back as enormously inflated strings.
// For now, we'll just make them dummy values.
if (typeof response.resolution.payout.buyerOutput.amount === 'string') {
response.resolution.payout.buyerOutput.amount = 25000;
}

if (typeof response.resolution.payout.vendorOutput.amount === 'string') {
response.resolution.payout.vendorOutput.amount = 12000;
}

if (typeof response.resolution.payout.moderatorOutput.amount === 'string') {
response.resolution.payout.moderatorOutput.amount = 6000;
}

response.resolution.payout.buyerOutput.amount =
integerToDecimal(response.resolution.payout.buyerOutput.amount || 0, true);
response.resolution.payout.vendorOutput.amount =
integerToDecimal(response.resolution.payout.vendorOutput.amount || 0, true);
response.resolution.payout.moderatorOutput.amount =
integerToDecimal(response.resolution.payout.moderatorOutput.amount || 0, true);
}
}

return response;
Expand Down
11 changes: 11 additions & 0 deletions js/models/order/Contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ export default class extends BaseModel {
.get('contractType');
}

get isLocalPickup() {
const buyerOrder = this.get('buyerOrder');

if (buyerOrder && buyerOrder.items && buyerOrder.items[0] &&
buyerOrder.items[0].shippingOption) {
return buyerOrder.items[0].shippingOption.service === '';
}

return false;
}

parse(response) {
return {
...response,
Expand Down
35 changes: 35 additions & 0 deletions js/models/order/Order.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,41 @@ export default class extends BaseModel {
// convert price fields
response.contract.buyerOrder.payment.amount =
integerToDecimal(response.contract.buyerOrder.payment.amount, true);

if (response.contract.disputeResolution) {
response.contract.disputeResolution.payout.buyerOutput =
response.contract.disputeResolution.payout.buyerOutput || {};
response.contract.disputeResolution.payout.vendorOutput =
response.contract.disputeResolution.payout.vendorOutput || {};
response.contract.disputeResolution.payout.moderatorOutput =
response.contract.disputeResolution.payout.moderatorOutput || {};

// Temporary to account for server bug:
// https://github.com/OpenBazaar/openbazaar-go/issues/548
// Sometimes the payment amounts are coming back as enormously inflated strings.
// For now, we'll just make them dummy values.
if (typeof response.contract.disputeResolution.payout.buyerOutput.amount === 'string') {
response.contract.disputeResolution.payout.buyerOutput.amount = 25000;
}

if (typeof response.contract.disputeResolution.payout.vendorOutput.amount === 'string') {
response.contract.disputeResolution.payout.vendorOutput.amount = 12000;
}

if (typeof response.contract.disputeResolution.payout.moderatorOutput.amount === 'string') {
response.contract.disputeResolution.payout.moderatorOutput.amount = 6000;
}

response.contract.disputeResolution.payout.buyerOutput.amount =
integerToDecimal(
response.contract.disputeResolution.payout.buyerOutput.amount || 0, true);
response.contract.disputeResolution.payout.vendorOutput.amount =
integerToDecimal(
response.contract.disputeResolution.payout.vendorOutput.amount || 0, true);
response.contract.disputeResolution.payout.moderatorOutput.amount =
integerToDecimal(
response.contract.disputeResolution.payout.moderatorOutput.amount || 0, true);
}
}

response.paymentAddressTransactions = response.paymentAddressTransactions || [];
Expand Down
26 changes: 26 additions & 0 deletions js/models/order/OrderDispute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import app from '../../app';
import BaseModel from '../BaseModel';

export default class extends BaseModel {
defaults() {
return {
claim: '',
};
}

url() {
return app.getServerUrl('ob/opendispute/');
}

get idAttribute() {
return 'orderId';
}

sync(method, model, options) {
if (method === 'create' || method === 'update') {
options.type = 'POST';
}

return super.sync(method, model, options);
}
}
83 changes: 83 additions & 0 deletions js/models/order/ResolveDispute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import app from '../../app';
import BaseModel from '../BaseModel';

export default class extends BaseModel {
defaults() {
return {
resolution: '',
};
}

url() {
return app.getServerUrl('ob/closedispute/');
}

get idAttribute() {
return 'orderId';
}

validate(attrs) {
const errObj = {};

const addError = (fieldName, error) => {
errObj[fieldName] = errObj[fieldName] || [];
errObj[fieldName].push(error);
};

let vendorPercentageOk = false;
let buyerPercentageOk = false;

if (typeof attrs.vendorPercentage === 'undefined' || attrs.vendorPercentage === '') {
addError('vendorPercentage',
app.polyglot.t('resolveDisputeModelErrors.provideAmount'));
} else if (typeof attrs.vendorPercentage !== 'number') {
addError('vendorPercentage',
app.polyglot.t('resolveDisputeModelErrors.providePercentageAsNumber'));
} else if (attrs.vendorPercentage < 0 || attrs.vendorPercentage > 100) {
addError('vendorPercentage',
app.polyglot.t('resolveDisputeModelErrors.vendorPercentageOutOfRange'));
} else {
vendorPercentageOk = true;
}

if (typeof attrs.buyerPercentage === 'undefined' || attrs.buyerPercentage === '') {
addError('buyerPercentage',
app.polyglot.t('resolveDisputeModelErrors.provideAmount'));
} else if (typeof attrs.buyerPercentage !== 'number') {
addError('buyerPercentage',
app.polyglot.t('resolveDisputeModelErrors.providePercentageAsNumber'));
} else if (attrs.buyerPercentage < 0 || attrs.buyerPercentage > 100) {
addError('buyerPercentage',
app.polyglot.t('resolveDisputeModelErrors.buyerPercentageOutOfRange'));
} else {
buyerPercentageOk = true;
}

if (vendorPercentageOk && buyerPercentageOk) {
if (attrs.buyerPercentage + attrs.vendorPercentage > 100) {
addError('buyerPercentage',
app.polyglot.t('resolveDisputeModelErrors.totalPercentageOutOfRange'));
} else if (attrs.buyerPercentage + attrs.vendorPercentage < 100) {
addError('buyerPercentage',
app.polyglot.t('resolveDisputeModelErrors.totalPercentageTooLow'));
}
}

if (!attrs.resolution) {
addError('resolution',
app.polyglot.t('resolveDisputeModelErrors.provideResolution'));
}

if (Object.keys(errObj).length) return errObj;

return undefined;
}

sync(method, model, options) {
if (method === 'create' || method === 'update') {
options.type = 'POST';
}

return super.sync(method, model, options);
}
}
14 changes: 10 additions & 4 deletions js/models/order/orderFulfillment/OrderFulfillment.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,33 @@ export default class extends BaseModel {
throw new Error('Please provide the contract type.');
}

if (typeof options.isLocalPickup !== 'boolean') {
throw new Error('Please provide a boolean indicating whether the item is to ' +
'be picked up locally.');
}

// Since the contract type is not available on this when
// the defaults are initially called, we need to set the
// initial contract type dependant attributes here. We also
// set them in defaults, so if the model is reset, they'll
// be restored properly.
if (options.contractType === 'DIGITAL_GOOD') {
attrs.digitalDelivery = new DigitalDelivery();
} else if (options.contractType === 'PHYSICAL_GOOD') {
attrs.physicalDelivery = new PhysicalDelivery();
attrs.digitalDelivery = new DigitalDelivery(attrs.digitalDelivery || {});
} else if (options.contractType === 'PHYSICAL_GOOD' && !options.isLocalPickup) {
attrs.physicalDelivery = new PhysicalDelivery(attrs.physicalDelivery || {});
}

super(attrs, options);
this.contractType = options.contractType;
this.isLocalPickup = options.isLocalPickup;
}

defaults() {
const defaults = {};

if (this.contractType === 'DIGITAL_GOOD') {
defaults.digitalDelivery = new DigitalDelivery();
} else if (this.contractType === 'PHYSICAL_GOOD') {
} else if (this.contractType === 'PHYSICAL_GOOD' && !this.isLocalPickup) {
defaults.physicalDelivery = new PhysicalDelivery();
}

Expand Down
6 changes: 6 additions & 0 deletions js/templates/modals/orderDetail/actionBar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<% if (ob.showDisputeOrderButton) { %>
<%= ob.processingButton({
className: 'flex btn clrErr clrBrDec1 clrTOnEmph js-openDispute',
btnText: ob.polyT('orderDetail.actionBar.disputeOrderBtn'),
}) %>
<% } %>
Loading

0 comments on commit da72b26

Please sign in to comment.