diff --git a/Brocfile.js b/Brocfile.js index 436c8b025..4090733db 100644 --- a/Brocfile.js +++ b/Brocfile.js @@ -35,7 +35,7 @@ var fonts = pickFiles('bower_components/strapped/static/fonts', { var images = pickFiles('bower_components/strapped/static/images', { srcDir: '/', - files: ['**/*.png', '**/*.gif'], + files: ['**/*.png'], destDir: '/images' }); diff --git a/CHANGELOG.md b/CHANGELOG.md index 493d9dde5..acaa6d5c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Balanced Dashboard Changelog - ### master +* Refactoring how model sidebars are displayed +* Turning off marketplace application process temporarily * Fixing restart verification button not showing after the wait period is over ### 1.2.1 diff --git a/Gruntfile.js b/Gruntfile.js index 820b59c0d..12c7041d6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -201,7 +201,7 @@ module.exports = function(grunt) { /* grunt task commands */ - grunt.registerTask('default', ['clean', 'bower', 'copy', 'exec:ember_server']); + grunt.registerTask('default', ['clean', 'copy', 'exec:ember_server']); grunt.registerTask('test', ['bower:install', 'exec:ember_test']); grunt.registerTask('build', ['bower:install', 'exec:ember_build_production']); grunt.registerTask('deploy', ['admin:uninstall', 'build', 's3:productionCached', 's3:productionUncached']); diff --git a/app/controllers/marketplace.js b/app/controllers/marketplace.js index 840eab7de..5d719cbec 100644 --- a/app/controllers/marketplace.js +++ b/app/controllers/marketplace.js @@ -29,18 +29,15 @@ var MarketplaceController = Ember.ObjectController.extend({ return this.get("auth.signedIn") && this.get("model"); }.property("auth.signedIn", "model"), - transactionSelected: isSelected('marketplace.transactions', 'credits', 'debits', 'holds', 'refunds', 'reversals'), - orderSelected: isSelected('marketplace.orders', 'orders'), - customerSelected: isSelected('marketplace.customers', 'customer'), - fundingInstrumentSelected: isSelected('marketplace.funding_instruments', 'bank_accounts', 'cards'), + orderSelected: isSelected('marketplace.orders', 'orders', 'credits', 'debits', 'holds', 'refunds', 'reversals'), + settlementSelected: isSelected('marketplace.settlements', 'settlement'), disputeSelected: isSelected('marketplace.disputes', 'dispute'), + customerSelected: isSelected('marketplace.customers', 'customer'), + fundingInstrumentSelected: isSelected('marketplace.funding_instruments', 'bank_accounts', 'cards', 'account'), logSelected: isSelected('marketplace.logs', 'log'), invoiceSelected: isSelected('marketplace.invoices', 'invoice'), settingSelected: isSelected('marketplace.settings'), - // Note: need this since bind-attr only works for a single property - paymentSelected: Ember.computed.or('transactionSelected', 'orderSelected'), - disputesResultsLoader: function() { if (this.get("model")) { return this.get('model').getDisputesLoader({ diff --git a/app/controllers/marketplace/import-payouts.js b/app/controllers/marketplace/import-payouts.js index d210298bc..2025c26ff 100644 --- a/app/controllers/marketplace/import-payouts.js +++ b/app/controllers/marketplace/import-payouts.js @@ -51,7 +51,7 @@ var MarketplaceImportPayoutsController = Ember.Controller.extend(Ember.Evented, callback(); } var count = collection.filterBy('isSaved').get('length'); - self.transitionToRoute('marketplace.transactions'); + self.transitionToRoute('marketplace.orders'); self.refresh(''); var message = '%@ payouts were successfully submitted. Payouts might take a couple seconds to appear in the transactions list.'.fmt(count); diff --git a/app/controllers/marketplace/settings.js b/app/controllers/marketplace/settings.js index c96137c50..e518d9f65 100644 --- a/app/controllers/marketplace/settings.js +++ b/app/controllers/marketplace/settings.js @@ -10,6 +10,16 @@ var MarketplaceSettingsController = Ember.ObjectController.extend(actionsMixin, ownerCustomer: Ember.computed.oneWay("marketplace.owner_customer"), + accountsResultsLoader: function() { + if (this.get("owner_customer")) { + return this.get("owner_customer").getAccountsLoader({ + limit: 10 + }); + } else { + return this.get("container").lookup("results-loader:base"); + } + }.property("owner_customer"), + fundingInstrumentsResultsLoader: function() { if (this.get("owner_customer")) { return this.get("owner_customer").getFundingInstrumentsLoader({ diff --git a/app/controllers/marketplace/settlements.js b/app/controllers/marketplace/settlements.js new file mode 100644 index 000000000..0e82d77ef --- /dev/null +++ b/app/controllers/marketplace/settlements.js @@ -0,0 +1,16 @@ +import Ember from "ember"; + +var MarketplaceSettlementsController = Ember.ObjectController.extend({ + needs: ['marketplace'], + resultsLoader: Ember.computed.oneWay("model"), + actions: { + changeDateFilter: function(startTime, endTime) { + this.get("resultsLoader").setProperties({ + endTime: endTime, + startTime: startTime + }); + }, + } +}); + +export default MarketplaceSettlementsController; diff --git a/app/initializers/legacy-routes-initializer.coffee b/app/initializers/legacy-routes-initializer.coffee index 8db7d59e2..02195e5af 100644 --- a/app/initializers/legacy-routes-initializer.coffee +++ b/app/initializers/legacy-routes-initializer.coffee @@ -7,18 +7,15 @@ LegacyRoutesInitializer = klass = createRedirectRoute(createArgs...) container.register("route:#{name}", klass) - defineRoute("account", "customer", "account") - defineRoute("accounts", "marketplace.customer") - defineRoute("accounts.index", 'marketplace.customers', "accounts") - defineRoute("bank-account.index", 'activity.funding_instruments') defineRoute("cards.index", "activity.funding_instruments") - defineRoute("marketplace-redirect-activity-transactions", "marketplace.transactions") - defineRoute("marketplace-redirect-activity-orders", "activity.orders") - defineRoute("marketplace-redirect-activity-customers", "marketplace.customers") - defineRoute("marketplace-redirect-activity-funding-instruments", "marketplace.funding-instruments") - defineRoute("marketplace-redirect-activity-disputes", "marketplace.disputes") - defineRoute("marketplace-redirect-invoices", "marketplace.invoices") + defineRoute("marketplace.redirect-activity-transactions", "marketplace.orders") + defineRoute("marketplace.redirect-transactions", "marketplace.orders") + defineRoute("marketplace.redirect-activity-orders", "marketplace.orders") + defineRoute("marketplace.redirect-activity-customers", "marketplace.customers") + defineRoute("marketplace.redirect-activity-funding-instruments", "marketplace.funding-instruments") + defineRoute("marketplace.redirect-activity-disputes", "marketplace.disputes") + defineRoute("marketplace.redirect-invoices", "marketplace.invoices") `export default LegacyRoutesInitializer` diff --git a/app/initializers/result-loaders-initializer.coffee b/app/initializers/result-loaders-initializer.coffee index ab341f2f7..322547c53 100644 --- a/app/initializers/result-loaders-initializer.coffee +++ b/app/initializers/result-loaders-initializer.coffee @@ -12,6 +12,7 @@ LOADER_NAMES = [ "marketplace-search" "orders" "transactions" + "unsettled-transactions" ] ResultLoadersInitializer = diff --git a/app/initializers/type-mappings-initializer.coffee b/app/initializers/type-mappings-initializer.coffee index 150d55671..342ccae2f 100644 --- a/app/initializers/type-mappings-initializer.coffee +++ b/app/initializers/type-mappings-initializer.coffee @@ -9,6 +9,7 @@ TypeMappingsInitializer = TypeMappings.addTypeMapping(key, klass) registerMapping "api_key", "api-key" + registerMapping "account" registerMapping "bank_account", "bank-account" registerMapping "bank-account", "bank-account" registerMapping "bank_account_verification", "verification" diff --git a/app/lib/utils.js b/app/lib/utils.js index bcb651e6b..84ff7b1ba 100644 --- a/app/lib/utils.js +++ b/app/lib/utils.js @@ -239,53 +239,6 @@ var Utils = Ember.Namespace.create({ } }, - applyUriFilters: function(uri, params) { - if (!uri) { - return uri; - } - - var transformedParams = ['limit', 'offset', 'sortField', 'sortOrder', 'minDate', 'maxDate', 'type', 'query']; - - var filteringParams = { - limit: params.limit || 10, - offset: params.offset || 0 - }; - - if (params.sortField && params.sortOrder && params.sortOrder !== 'none') { - filteringParams.sort = params.sortField + ',' + params.sortOrder; - } - - if (params.minDate) { - filteringParams['created_at[>]'] = params.minDate.toISOString(); - } - if (params.maxDate) { - filteringParams['created_at[<]'] = params.maxDate.toISOString(); - } - if (params.type) { - switch (params.type) { - case 'search': - filteringParams['type[in]'] = Constants.SEARCH.SEARCH_TYPES.join(','); - break; - case 'transaction': - filteringParams['type[in]'] = Constants.SEARCH.TRANSACTION_TYPES.join(','); - break; - case 'funding_instrument': - filteringParams['type[in]'] = Constants.SEARCH.FUNDING_INSTRUMENT_TYPES.join(','); - break; - default: - filteringParams.type = params.type; - } - } - filteringParams.q = ''; - if (params.query && params.query !== '%') { - filteringParams.q = params.query; - } - - filteringParams = _.extend(filteringParams, _.omit(params, transformedParams)); - filteringParams = Utils.sortDict(filteringParams); - return this.buildUri(uri, filteringParams); - }, - buildUri: function(path, queryStringObject) { var queryString = _.isString(queryStringObject) ? queryStringObject : diff --git a/app/models/account.coffee b/app/models/account.coffee new file mode 100644 index 000000000..1ad979004 --- /dev/null +++ b/app/models/account.coffee @@ -0,0 +1,24 @@ +`import Ember from "ember";` +`import Model from "./core/model";` +`import Computed from "balanced-dashboard/utils/computed";` +`import Constants from "balanced-dashboard/utils/constants";` + +Account = Model.extend( + routeName: "account" + route_name: "account" + isPayableAccount: Ember.computed.equal("type", "payable") + appears_on_statement_max_length: Constants.MAXLENGTH.APPEARS_ON_STATEMENT_BANK_ACCOUNT, + + type_name: Ember.computed "type", -> + type = @get("type") + + if type + "#{type.capitalize()} account" + else + "account" + + description_with_type: Ember.computed "id", -> + "Payable account: #{@get("id")}" +) + +`export default Account;` diff --git a/app/models/bk/account.coffee b/app/models/bk/account.coffee new file mode 100644 index 000000000..910cc2025 --- /dev/null +++ b/app/models/bk/account.coffee @@ -0,0 +1,20 @@ +`import Ember from "ember";` +`import BkAccount from "balanced-addon-models/models/account";` +`import Computed from "balanced-dashboard/utils/computed";` +`import BkUtils from "balanced-dashboard/utils/bk-utils";` + +Account = BkAccount.extend( + routeName: "account", + route_name: "account", + isPayableAccount: Ember.computed.equal("type", "payable"), + type_name: (-> + type = @get("type").capitalize() + "%@ account".fmt(type) + ).property("type"), + description_with_type: Ember.computed("id", -> + "Payable account: #{@get("id")}" + ), + toLegacyModel: BkUtils.generateToLegacyModelMethod("account") +) + +`export default Account;` diff --git a/app/models/bk/bank-account.coffee b/app/models/bk/bank-account.coffee index 19188a3c6..2cc02dbb6 100644 --- a/app/models/bk/bank-account.coffee +++ b/app/models/bk/bank-account.coffee @@ -1,7 +1,10 @@ `import Ember from "ember";` -`import BankAccount from "balanced-addon-models/models/bank-account";` +`import BkBankAccount from "balanced-addon-models/models/bank-account";` +`import BkUtils from "balanced-dashboard/utils/bk-utils";` -BkBankAccount = BankAccount.extend( +BankAccount = BkBankAccount.extend( + routeName: "bank_accounts" + toLegacyModel: BkUtils.generateToLegacyModelMethod("bank_account") ) -`export default BkBankAccount;` +`export default BankAccount;` diff --git a/app/models/bk/customer.coffee b/app/models/bk/customer.coffee index 355b44f48..0bdffe6d7 100644 --- a/app/models/bk/customer.coffee +++ b/app/models/bk/customer.coffee @@ -2,7 +2,7 @@ `import BkCustomer from "balanced-addon-models/models/customer";` Customer = BkCustomer.extend( - routeName: "customer" + routeName: "customer", ) `export default Customer;` diff --git a/app/models/bk/settlement.coffee b/app/models/bk/settlement.coffee new file mode 100644 index 000000000..233e978fa --- /dev/null +++ b/app/models/bk/settlement.coffee @@ -0,0 +1,13 @@ +`import Ember from "ember";` +`import Utils from "balanced-dashboard/lib/utils";` +`import BkSettlement from "balanced-addon-models/models/settlement";` + +Settlement = BkSettlement.extend( + routeName: "settlement", + type_name: "Settlement", + amountInDollars: (-> + "$%@".fmt(Utils.centsToDollars(@get("amount"))) + ).property("amount") +) + +`export default Settlement;` diff --git a/app/models/card-validatable.coffee b/app/models/card-validatable.coffee deleted file mode 100644 index 136fce6eb..000000000 --- a/app/models/card-validatable.coffee +++ /dev/null @@ -1,18 +0,0 @@ -`import Card from "./card";` -`import FundingInstrumentValidatable from "./mixins/funding-instrument-validatable";` - -CardValidatable = Card.extend(Ember.Validations, FundingInstrumentValidatable, - getTokenizingResponseHref: (response) -> - response.cards[0].href - getTokenizingObject: -> - balanced.card - getTokenizingData: -> - number: @get('number'), - expiration_month: @get('expiration_month'), - expiration_year: @get('expiration_year'), - cvv: @get('cvv'), - name: @get('name'), - address: @get('address') || {} -) - -`export default CardValidatable;` diff --git a/app/models/card.js b/app/models/card.js index f4b9aad97..fb1cac200 100644 --- a/app/models/card.js +++ b/app/models/card.js @@ -23,6 +23,21 @@ var Card = FundingInstrument.extend(Ember.Validations, { expiration_year: { presence: true }, + expiration_date: { + presence: true, + format: Constants.EXPIRATION_DATE_FORMAT, + expired: { + validator: function(object, attrName, value) { + var date = object.getExpirationDate(); + if (Ember.isBlank(date)) { + object.get("validationErrors").add(attrName, "expired", null, "" + value + " is not a valid card expiration date"); + } + else if (date < new Date()) { + object.get("validationErrors").add(attrName, "expired", null, "is expired"); + } + } + } + }, cvv: { presence: true, numericality: true, @@ -85,24 +100,48 @@ var Card = FundingInstrument.extend(Ember.Validations, { ); }.property('name', 'last_four', 'brand'), - human_readable_expiration: Computed.fmt('expiration_month', 'expiration_year', '%@/%@'), + human_readable_expiration: Ember.computed.reads('expiration_date'), - tokenizeAndCreate: function(customerId) { - var self = this; - var promise = this.resolveOn('didCreate'); - - function errorCreatingCard(err) { - Ember.run.next(function() { - self.setProperties({ - displayErrorDescription: true, - isSaving: false, - errorDescription: 'There was an error processing your card. ' + (Ember.get(err, 'errorDescription') || ''), - validationErrors: Ember.get(err, 'validationErrors') || {} + expiration_date: function(attrName, value) { + if (arguments.length) { + var match = this.getExpirationDateMatch(value); + if (match) { + this.setProperties({ + expiration_month: match[1], + expiration_year: match[2] }); - }); + } + } + return "%@ / %@".fmt(this.get("expiration_month"), this.get("expiration_year")); + }.property("expiration_month", "expiration_year"), + - promise.reject(); + getExpirationDate: function() { + var match = this.getExpirationDateMatch(this.get("expiration_date")); + if (match) { + var month = parseInt(match[0]); + if (0 < month && month <= 12) { + return moment(match[0], "MM / YYYY").endOf("month").toDate(); + } } + }, + + getExpirationDateMatch: function(expirationDate) { + if (!Ember.isBlank(expirationDate)) { + return expirationDate.match(Constants.EXPIRATION_DATE_FORMAT); + } + }, + + tokenizeAndCreate: function(customerId) { + var self = this; + + var deferred = Ember.RSVP.defer(); + + var getErrorMessage = function(error) { + return Ember.isBlank(error.additional) ? + error.description : + error.additional; + }; this.set('isSaving', true); var cardData = { @@ -118,46 +157,51 @@ var Card = FundingInstrument.extend(Ember.Validations, { cardData.customer = customerId; } - // Tokenize the card using the balanced.js library balanced.card.create(cardData, function(response) { if (response.errors) { - var validationErrors = Utils.extractValidationErrorHash(response); - self.setProperties({ - validationErrors: validationErrors, - isSaving: false + response.errors.forEach(function(error) { + if (Ember.isBlank(error.extras)) { + self.get("validationErrors").add(undefined, "server", null, getErrorMessage(error)); + } + else { + _.each(error.extras, function(value, key) { + self.get("validationErrors").add(key, "server", null, value); + }); + } }); - - if (!validationErrors) { - self.set('displayErrorDescription', true); - var errorSuffix = (response.errors && response.errors.length > 0 && response.errors[0].description) ? (': ' + response.errors[0].description) : '.'; - self.setProperties({ - displayErrorDescription: true, - errorDescription: 'Sorry, there was an error tokenizing this card' + errorSuffix - }); - } - - promise.reject(validationErrors); + self.set("isSaving", false); + deferred.reject(response); } else { - Card.find(response.cards[0].href) - - // Now that it's been tokenized, we just need to associate it with the customer's account - .then(function(card) { - card.set('links.customer', customerId); - - card.save().then(function() { + Card.findCreatedCard(response.cards[0].href) + .then(function(card) { + card.set('links.customer', customerId); + return card.save(); + }) + .then(function(card) { self.setProperties({ isSaving: false, isNew: false, isLoaded: true }); - - self.trigger('didCreate', card); - }, errorCreatingCard); - }, errorCreatingCard); + deferred.resolve(card); + }, function (response) { + response.errors.forEach(function(error) { + if (Ember.isBlank(error.extras)) { + self.get("validationErrors").add(undefined, "server", null, getErrorMessage(error)); + } + else { + _.each(error.extras, function(value, key) { + self.get("validationErrors").add(key, "server", null, value); + }); + } + }); + self.set("isSaving", false); + deferred.reject(response); + }); } }); - return promise; + return deferred.promise; } }); diff --git a/app/models/core/search-model-array.js b/app/models/core/search-model-array.js index faef7b838..0215940a4 100644 --- a/app/models/core/search-model-array.js +++ b/app/models/core/search-model-array.js @@ -19,7 +19,8 @@ var SearchModelArray = ModelArray.extend(Ember.SortableMixin, { total_reversals: readOnly("reversal"), total_transactions: Computed.sumAll('total_credits', 'total_debits', 'total_card_holds', 'total_refunds', "total_reversals"), total_funding_instruments: Computed.sumAll('total_bank_accounts', 'total_cards'), - total_results: Computed.sumAll('total_orders', 'total_transactions', 'total_funding_instruments', 'total_customers') + total_settlements: readOnly('settlement'), + total_results: Computed.sumAll('total_orders', 'total_transactions', 'total_funding_instruments', 'total_customers', 'total_settlements') }); export default SearchModelArray; diff --git a/app/models/credit.js b/app/models/credit.js index 5a0c2b3a1..e0293fb0b 100644 --- a/app/models/credit.js +++ b/app/models/credit.js @@ -1,7 +1,7 @@ import Ember from "ember"; import Computed from "balanced-dashboard/utils/computed"; import Transaction from "./transaction"; -import Rev1Serializer from "../serializers/rev1"; +import TransactionSerializer from "../serializers/transaction"; import Model from "./core/model"; import Utils from "balanced-dashboard/lib/utils"; @@ -14,6 +14,7 @@ var Credit = Transaction.extend({ destination: Model.belongsTo('destination', 'funding-instrument'), reversals: Model.hasMany('reversals', 'reversal'), order: Model.belongsTo('order', 'order'), + settlement: Model.belongsTo('settlement', 'settlement'), funding_instrument_description: Ember.computed.alias('destination.description'), last_four: Ember.computed.alias('destination.last_four'), @@ -70,7 +71,7 @@ var Credit = Transaction.extend({ }); Credit.reopenClass({ - serializer: Rev1Serializer.extend({ + serializer: TransactionSerializer.extend({ serialize: function(record) { var json = this._super(record); @@ -83,11 +84,6 @@ Credit.reopenClass({ } } - if (!Ember.isBlank(json.order_uri)) { - json.order = json.order_uri; - delete json.order_uri; - } - return json; } }).create(), diff --git a/app/models/customer.js b/app/models/customer.js index 2a347ed63..6dbec8fd5 100644 --- a/app/models/customer.js +++ b/app/models/customer.js @@ -1,8 +1,12 @@ import { CountryCodesToNames } from "balanced-dashboard/lib/country-codes"; import Model from "./core/model"; +import Order from "./order"; import Computed from "balanced-dashboard/utils/computed"; import FundingInstrumentsResultsLoader from "./results-loaders/funding-instruments"; import TransactionsResultsLoader from "./results-loaders/transactions"; +import BuyerTransactionsResultsLoader from "./results-loaders/buyer-transactions"; +import MerchantTransactionsResultsLoader from "./results-loaders/merchant-transactions"; +import OrdersResultsLoader from "./results-loaders/orders"; var CUSTOMER_TYPES = { BUSINESS: 'Business', @@ -19,6 +23,8 @@ var Customer = Model.extend({ refunds: Model.hasMany('refunds', 'refund'), orders: Model.hasMany('orders', 'order'), disputes: Model.hasMany('disputes', 'dispute'), + accounts: Model.hasMany('accounts', 'account'), + account: Ember.computed.reads("accounts.firstObject"), uri: '/customers', route_name: 'customer', @@ -26,18 +32,6 @@ var Customer = Model.extend({ has_bank_account: Ember.computed.and('bank_accounts.isLoaded', 'bank_accounts.length'), - orders_list: function() { - var customer_uri = this.get('href'); - var orders = this.get('orders') || Ember.A(); - - if (customer_uri) { - orders = orders.filter(function(order) { - return order.get('merchant_uri') === customer_uri; - }); - } - return orders; - }.property('orders', 'orders.@each.merchant_uri'), - debitable_bank_accounts: function() { return this.get('bank_accounts').filterBy('can_debit'); }.property('bank_accounts.@each.can_debit'), @@ -52,7 +46,7 @@ var Customer = Model.extend({ funding_instruments: Ember.computed.union('bank_accounts', 'cards'), debitable_funding_instruments: Ember.computed.union('debitable_bank_accounts', 'cards'), - creditable_funding_instruments: Ember.computed.union('bank_accounts', 'creditable_cards'), + creditable_funding_instruments: Ember.computed.union('bank_accounts', 'creditable_cards', 'accounts'), getFundingInstrumentsLoader: function(attributes) { attributes = _.extend({ @@ -67,12 +61,54 @@ var Customer = Model.extend({ }, attributes); return DisputesResultsLoader.create(attributes); }, + + hasCreditableOrders: Ember.computed.reads("creditableOrders.length"), + + creditableOrders: function () { + return this.getOrdersLoader().get("results"); + }.property("uri"), + + getOrdersLoader: function(attributes) { + // Note: Replace this back to "order_uri" when the issue balanced-api #739 is fixed + attributes = _.extend({ + path: this.get("uri") + "/search", + typeFilters: "order" + }, attributes); + return OrdersResultsLoader.create(attributes); + }, + getBuyerTransactionsLoader: function(attributes) { + attributes = _.extend({ + path: this.get("transactions_uri"), + }, attributes); + return BuyerTransactionsResultsLoader.create(attributes); + }, + getMerchantTransactionsLoader: function(attributes) { + attributes = _.extend({ + path: this.get("transactions_uri"), + }, attributes); + return MerchantTransactionsResultsLoader.create(attributes); + }, getTransactionsLoader: function(attributes) { attributes = _.extend({ path: this.get("transactions_uri"), }, attributes); return TransactionsResultsLoader.create(attributes); }, + getAccountsLoader: function(attributes) { + var AccountsResultsLoader = require("balanced-dashboard/models/results-loaders/accounts")["default"]; + attributes = _.extend({ + path: this.get("accounts_uri"), + }, attributes); + return AccountsResultsLoader.create(attributes); + }, + + createOrder: function(description) { + var order = Order.create({ + description: description + }); + order.set("uri", this.get("orders_uri")); + return order.save(); + }, type: function() { return (this.get('ein') || this.get('business_name')) ? CUSTOMER_TYPES.BUSINESS : CUSTOMER_TYPES.PERSON; diff --git a/app/models/debit.js b/app/models/debit.js index 5b267940f..6a71abd5d 100644 --- a/app/models/debit.js +++ b/app/models/debit.js @@ -2,6 +2,7 @@ import Computed from "balanced-dashboard/utils/computed"; import Transaction from "./transaction"; import Model from "./core/model"; import Utils from "balanced-dashboard/lib/utils"; +import TransactionSerializer from "../serializers/transaction"; var Debit = Transaction.extend({ @@ -59,4 +60,8 @@ var Debit = Transaction.extend({ }.property('amount', 'refund_amount', 'is_succeeded', 'dispute') }); +Debit.reopenClass({ + serializer: TransactionSerializer.create() +}); + export default Debit; diff --git a/app/models/factories/credit-bank-account-transaction-factory.js b/app/models/factories/credit-bank-account-transaction-factory.js index d542b68f0..2ce3a47c0 100644 --- a/app/models/factories/credit-bank-account-transaction-factory.js +++ b/app/models/factories/credit-bank-account-transaction-factory.js @@ -1,25 +1,21 @@ import Ember from "ember"; import ValidationHelpers from "balanced-dashboard/utils/validation-helpers"; -import TransactionFactory from "./transaction-factory"; +import CreditOrderFactory from "./credit-order-factory"; +import BankAccount from "../bank-account"; /* * This factory uses the api feature of creating a Credit without creating a * BankAccount object. */ -var CreditBankAccountTransactionFactory = TransactionFactory.extend({ +var CreditBankAccountTransactionFactory = CreditOrderFactory.extend({ getDestinationAttributes: function() { return this.getProperties("account_number", "name", "routing_number", "account_type"); }, - getAttributes: function() { - var attributes = this.getProperties("amount", "appears_on_statement_as", "description"); - attributes.destination = this.getDestinationAttributes(); - return attributes; - }, - - save: function() { - var Credit = BalancedApp.__container__.lookupFactory("model:credit"); - return Credit.create(this.getAttributes()).save(); + getDestination: function(seller) { + return BankAccount + .create(this.getDestinationAttributes()) + .tokenizeAndCreate(seller.get("uri")); }, validations: { @@ -29,7 +25,7 @@ var CreditBankAccountTransactionFactory = TransactionFactory.extend({ name: ValidationHelpers.bankAccountName, routing_number: ValidationHelpers.bankAccountRoutingNumber, account_number: ValidationHelpers.bankAccountNumber, - account_type: ValidationHelpers.bankAccountType, + account_type: ValidationHelpers.bankAccountType } }); diff --git a/app/models/factories/credit-existing-funding-instrument-transaction-factory.js b/app/models/factories/credit-existing-funding-instrument-transaction-factory.js index cf1544be0..f57996b74 100644 --- a/app/models/factories/credit-existing-funding-instrument-transaction-factory.js +++ b/app/models/factories/credit-existing-funding-instrument-transaction-factory.js @@ -1,38 +1,55 @@ import Ember from "ember"; import ValidationHelpers from "balanced-dashboard/utils/validation-helpers"; -import TransactionFactory from "./transaction-factory"; +import Utils from "balanced-dashboard/lib/utils"; +import CreditOrderFactory from "./credit-order-factory"; -var CreditExistingFundingInstrumentTransactionFactory = TransactionFactory.extend({ +var CreditExistingFundingInstrumentTransactionFactory = CreditOrderFactory.extend({ appears_on_statement_max_length: Ember.computed.oneWay("destination.appears_on_statement_max_length"), - destination_uri: Ember.computed.readOnly("destination.uri"), - getCreditAttributes: function() { - var properties = this.getProperties("amount", "appears_on_statement_as", "description", "destination_uri"); - properties.uri = this.get("destination.credits_uri"); - - if (this.get("order.href")) { - properties.order_uri = this.get("order.href"); - } - return properties; + getDestination: function() { + return Ember.RSVP.resolve(this.get("destination")); }, - save: function() { - var Credit = BalancedApp.__container__.lookupFactory("model:credit"); - - this.validate(); - if (this.get("isValid")) { - return Credit.create(this.getCreditAttributes()).save(); - } else { - return Ember.RSVP.reject(); + isAmountOverMaximum: function() { + if (this.get("order")) { + return this.get("amount") > this.get("order.amount_escrowed"); } + return false; }, validations: { - destination_uri: { - presence: true + dollar_amount: { + format: { + validator: function(object, attribute, value) { + var message = function(message) { + object.get("validationErrors").add(attribute, "format", null, message); + }; + + value = (value || "").toString().trim(); + if (Ember.isBlank(value)) { + message("is required"); + } else if (object.isAmountOverMaximum()) { + var maxAmount = object.get("order.amount_escrowed"); + message("cannot be more than %@".fmt(Utils.formatCurrency(maxAmount))); + } else if (!object.isAmountPositive()) { + message("must be a positive number"); + } else { + try { + var v = Utils.dollarsToCents(value); + if (isNaN(v) || v <= 0) { + message("must be a positive number"); + } + } catch (e) { + message(e.message.replace("Error: ", "")); + } + } + } + } }, - dollar_amount: ValidationHelpers.positiveDollarAmount, - appears_on_statement_as: ValidationHelpers.bankTransactionAppearsOnStatementAs + appears_on_statement_as: ValidationHelpers.bankTransactionAppearsOnStatementAs, + destination: { + presence: true + } } }); diff --git a/app/models/factories/credit-order-factory.js b/app/models/factories/credit-order-factory.js new file mode 100644 index 000000000..e92236873 --- /dev/null +++ b/app/models/factories/credit-order-factory.js @@ -0,0 +1,101 @@ +import TransactionFactory from "./transaction-factory"; +import Credit from "../credit"; +import Customer from "../customer"; + +var CreditOrderFactory = TransactionFactory.extend({ + save: function() { + var self = this; + var order = this.get("order"); + + var deferred = Ember.RSVP.defer(); + this.validate(); + + var getErrorMessage = function(error) { + return Ember.isBlank(error.additional) ? + error.description : + error.additional; + }; + + if (this.get("isValid")) { + self.getSeller() + .then(function(seller) { + return self.getDestination(seller); + }) + .then(function(destination) { + if (order) { + return self.createCredit(destination, order); + } else { + return self.createOneOffCredit(destination); + } + }) + .then(function(credit) { + deferred.resolve(credit); + }) + .catch(function(response) { + response.errors.forEach(function(error) { + if (error.extras) { + _.each(error.extras, function(value, key) { + self.get("validationErrors").add(key, "server", null, value); + }); + } + self.get("validationErrors").add(undefined, "server", null, getErrorMessage(error)); + }); + deferred.reject(self); + }); + } else { + deferred.reject(); + } + + return deferred.promise; + }, + + getDestination: function(/* seller */) { + Ember.assert("Implement #getDestination and make it return a promise with the destination", false); + }, + + getSeller: function() { + var seller = this.get("order.seller"); + + if (seller) { + return Ember.RSVP.resolve(seller); + } else { + return Customer.create({ + name: this.get("name") + }).save(); + } + }, + + getCreditAttributes: function() { + var properties = this.getProperties("amount", "appears_on_statement_as"); + properties.description = this.get("credit_description"); + + return properties; + }, + + createOneOffCredit: function(destination) { + var destinationUri = destination.get("uri"); + var creditsUri = destination.get("credits_uri"); + + var creditAttributes = _.extend({}, this.getCreditAttributes(), { + uri: creditsUri, + destination_uri: destinationUri, + }); + + return Credit.create(creditAttributes).save(); + }, + + createCredit: function(destination, order) { + var destinationUri = destination.get("uri"); + var creditsUri = destination.get("credits_uri"); + + var creditAttributes = _.extend({}, this.getCreditAttributes(), { + uri: creditsUri, + destination_uri: destinationUri, + order_uri: order.get("uri") + }); + + return Credit.create(creditAttributes).save(); + } +}); + +export default CreditOrderFactory; diff --git a/app/models/factories/debit-card-transaction-factory.js b/app/models/factories/debit-card-transaction-factory.js index f2601772a..a6809e3df 100644 --- a/app/models/factories/debit-card-transaction-factory.js +++ b/app/models/factories/debit-card-transaction-factory.js @@ -1,12 +1,11 @@ -import Debit from "../debit"; -import Card from "../card"; -import TransactionFactory from "./transaction-factory"; +import DebitOrderFactory from "./debit-order-factory"; import ValidationHelpers from "balanced-dashboard/utils/validation-helpers"; +import Customer from "../customer"; +import Card from "../card"; +import Constants from "balanced-dashboard/utils/constants"; -var EXPIRATION_DATE_FORMAT = /^(\d\d) [\/-] (\d\d\d\d)$/; - -var DebitCardTransactionFactory = TransactionFactory.extend({ - getDestinationAttributes: function() { +var DebitCardTransactionFactory = DebitOrderFactory.extend({ + getSourceAttributes: function() { var attributes = this.getProperties("name", "number", "cvv", "expiration_month", "expiration_year"); attributes.address = { postal_code: this.get("postal_code") @@ -14,10 +13,6 @@ var DebitCardTransactionFactory = TransactionFactory.extend({ return attributes; }, - getDebitAttributes: function() { - return this.getProperties("amount", "appears_on_statement_as", "description"); - }, - validations: { dollar_amount: ValidationHelpers.positiveDollarAmount, appears_on_statement_as: ValidationHelpers.cardTransactionAppearsOnStatementAs, @@ -27,11 +22,14 @@ var DebitCardTransactionFactory = TransactionFactory.extend({ cvv: ValidationHelpers.cardCvv, expiration_date: { presence: true, - format: EXPIRATION_DATE_FORMAT, + format: Constants.EXPIRATION_DATE_FORMAT, expired: { validator: function(object, attrName, value) { var date = object.getExpirationDate(); - if (date < new Date()) { + if (Ember.isBlank(date)) { + object.get("validationErrors").add(attrName, "expired", null, "" + value + " is not a valid card expiration date"); + } + else if (date < new Date()) { object.get("validationErrors").add(attrName, "expired", null, "is expired"); } } @@ -39,17 +37,47 @@ var DebitCardTransactionFactory = TransactionFactory.extend({ } }, + getSource: function(buyer) { + return Card + .create(this.getSourceAttributes()) + .tokenizeAndCreate(buyer.get("uri")); + }, + + getBuyerCustomerAttributes: function() { + var email = this.get("buyer_email_address"); + if (Ember.isBlank(email)) { + email = undefined; + } + return { + name: this.get("buyer_name"), + email: email + }; + }, + + getBuyer: function() { + var customer = this.get("customer"); + + if (customer) { + return customer; + } else { + return Customer.create(this.getBuyerCustomerAttributes()).save(); + } + }, + getExpirationDate: function() { var match = this.getExpirationDateMatch(); if (match) { - return moment(match[0], "MM / YYYY").endOf("month").toDate(); + var month = parseInt(match[0]); + if (0 < month && month <= 12) { + return moment(match[0], "MM / YYYY").endOf("month").toDate(); + } } }, getExpirationDateMatch: function() { var expirationDate = this.get("expiration_date"); if (!Ember.isBlank(expirationDate)) { - return expirationDate.match(EXPIRATION_DATE_FORMAT); + return expirationDate.match(Constants.EXPIRATION_DATE_FORMAT); } }, @@ -65,72 +93,7 @@ var DebitCardTransactionFactory = TransactionFactory.extend({ if (match) { return match[2]; } - }.property("expiration_date"), - - saveCard: function() { - var attributes = this.getDestinationAttributes(); - var deferred = Ember.RSVP.defer(); - - function resolve(r) { - deferred.resolve(r); - } - function reject(r) { - deferred.reject(r); - } - window.balanced.card.create(attributes, function(response) { - if (response.status_code === 201) { - Card.findCreatedCard(response.cards[0].href).then(resolve, reject); - } - else { - reject(response); - } - }); - return deferred.promise; - }, - - save: function() { - var deferred = Ember.RSVP.defer(); - - var baseDebitAttributes = this.getDebitAttributes(); - var self = this; - this.validate(); - - var getErrorMessage = function(error) { - return Ember.isBlank(error.additional) ? - error.description : - error.additional; - }; - - if (this.get("isValid")) { - this.saveCard() - .then(function(card) { - var debitAttributes = _.extend({}, baseDebitAttributes, { - uri: card.get('debits_uri'), - source_uri: card.get('uri') - }); - return Debit.create(debitAttributes).save(); - }) - .then(function(model) { - deferred.resolve(model); - }, function(response) { - response.errors.forEach(function(error) { - if (Ember.isBlank(error.extras)) { - self.get("validationErrors").add(undefined, "server", null, getErrorMessage(error)); - } - else { - _.each(error.extras, function(value, key) { - self.get("validationErrors").add(key, "server", null, value); - }); - } - }); - deferred.reject(self); - }); - } else { - deferred.reject(); - } - - return deferred.promise; - } + }.property("expiration_date") }); export default DebitCardTransactionFactory; diff --git a/app/models/factories/debit-existing-bank-account-transaction-factory.js b/app/models/factories/debit-existing-bank-account-transaction-factory.js deleted file mode 100644 index 18c6d515e..000000000 --- a/app/models/factories/debit-existing-bank-account-transaction-factory.js +++ /dev/null @@ -1,15 +0,0 @@ -import DebitExistingFundingInstrumentTransactionFactory from "./debit-existing-funding-instrument-transaction-factory"; -import ValidationHelpers from "balanced-dashboard/utils/validation-helpers"; - -var DebitExistingBankAccountTransactionFactory = DebitExistingFundingInstrumentTransactionFactory.extend({ - validations: { - dollar_amount: ValidationHelpers.positiveDollarAmount, - appears_on_statement_as: ValidationHelpers.bankTransactionAppearsOnStatementAs, - - source_uri: { - presence: true - } - } -}); - -export default DebitExistingBankAccountTransactionFactory; diff --git a/app/models/factories/debit-existing-card-transaction-factory.js b/app/models/factories/debit-existing-card-transaction-factory.js deleted file mode 100644 index 856433b05..000000000 --- a/app/models/factories/debit-existing-card-transaction-factory.js +++ /dev/null @@ -1,17 +0,0 @@ -import DebitExistingFundingInstrumentTransactionFactory from "./debit-existing-funding-instrument-transaction-factory"; -import ValidationHelpers from "balanced-dashboard/utils/validation-helpers"; -import Card from "../card"; -import Debit from "../debit"; - -var DebitExistingCardTransactionFactory = DebitExistingFundingInstrumentTransactionFactory.extend({ - validations: { - dollar_amount: ValidationHelpers.positiveDollarAmount, - appears_on_statement_as: ValidationHelpers.cardTransactionAppearsOnStatementAs, - - source_uri: { - presence: true - } - } -}); - -export default DebitExistingCardTransactionFactory; diff --git a/app/models/factories/debit-existing-funding-instrument-transaction-factory.js b/app/models/factories/debit-existing-funding-instrument-transaction-factory.js index 49c236b04..a5fb39cdd 100644 --- a/app/models/factories/debit-existing-funding-instrument-transaction-factory.js +++ b/app/models/factories/debit-existing-funding-instrument-transaction-factory.js @@ -1,38 +1,32 @@ -import TransactionFactory from "./transaction-factory"; +import DebitOrderFactory from "./debit-order-factory"; import ValidationHelpers from "balanced-dashboard/utils/validation-helpers"; -var DebitExistingFundingInstrumentTransactionFactory = TransactionFactory.extend({ +var DebitExistingFundingInstrumentTransactionFactory = DebitOrderFactory.extend({ appears_on_statement_max_length: Ember.computed.oneWay("source.appears_on_statement_max_length"), - source_uri: Ember.computed.readOnly("source.uri"), - getDebitAttributes: function() { - var properties = this.getProperties("amount", "appears_on_statement_as", "description", "source_uri"); - properties.uri = this.get("source.debits_uri"); - return properties; - }, - validations: { - dollar_amount: ValidationHelpers.positiveDollarAmount, - appears_on_statement_as: ValidationHelpers.cardTransactionAppearsOnStatementAs, + getBuyer: function() { + return Ember.RSVP.resolve(this.get("source.customer")); }, - save: function() { - var Debit = BalancedApp.__container__.lookupFactory("model:debit"); - var deferred = Ember.RSVP.defer(); + getSource: function() { + return Ember.RSVP.resolve(this.get("source")); + }, - this.validate(); - if (this.get("isValid")) { - Debit.create(this.getDebitAttributes()) - .save() - .then(function(model) { - deferred.resolve(model); - }, function() { - deferred.reject(); - }); - } else { - deferred.reject(); + validations: { + dollar_amount: ValidationHelpers.positiveDollarAmount, + appears_on_statement_as: { + presence: true, + format: { + validator: function(object, attribute) { + var validationErrors = object.get("validationErrors"); + var maxLength = object.get("appears_on_statement_max_length"); + ValidationHelpers.fundingInstrumentAppearsOnStatementAsValidator("appears_on_statement_as", object, maxLength); + } + } + }, + source: { + presence: true } - - return deferred.promise; } }); diff --git a/app/models/factories/debit-order-factory.js b/app/models/factories/debit-order-factory.js new file mode 100644 index 000000000..c2c1f1a69 --- /dev/null +++ b/app/models/factories/debit-order-factory.js @@ -0,0 +1,110 @@ +import TransactionFactory from "./transaction-factory"; +import Debit from "../debit"; +import Customer from "../customer"; + +var DebitOrderFactory = TransactionFactory.extend({ + save: function() { + var self = this; + var order; + var deferred = Ember.RSVP.defer(); + this.validate(); + + var getErrorMessage = function(error) { + return Ember.isBlank(error.additional) ? + error.description : + error.additional; + }; + + if (this.get("isValid")) { + this.getOrder() + .then(function(o) { + order = o; + return self.getBuyer(); + }) + .then(function(buyer) { + return self.getSource(buyer); + }) + .then(function(source) { + return self.createDebit(source, order); + }) + .then(function(debit) { + deferred.resolve(debit); + }) + .catch(function(response) { + response.errors.forEach(function(error) { + if (error.extras) { + _.each(error.extras, function(value, key) { + self.get("validationErrors").add(key, "server", null, value); + }); + } + self.get("validationErrors").add(undefined, "server", null, getErrorMessage(error)); + }); + deferred.reject(self); + }); + } else { + deferred.reject(); + } + + return deferred.promise; + }, + + getSource: function(/* buyer */) { + Ember.assert("Implement #getSource and make it return a promise with the source", false); + }, + + getBuyer: function() { + Ember.assert("Implement #getbuyer and make it return a promise with the buyer", false); + }, + + getDebitAttributes: function() { + var properties = this.getProperties("amount", "appears_on_statement_as"); + properties.description = this.get("debit_description"); + + return properties; + }, + + createDebit: function(source, order) { + var sourceUri = source.get("uri"); + var debitsUri = source.get("debits_uri"); + var buyerUri = source.get("customer_uri"); + + var debitAttributes = _.extend({}, this.getDebitAttributes(), { + customer_uri: buyerUri, + uri: debitsUri, + source_uri: sourceUri, + order_uri: order.get("uri") + }); + + return Debit.create(debitAttributes).save(); + }, + + getSellerCustomerAttributes: function() { + var email = this.get("seller_email_address"); + if (Ember.isBlank(email)) { + email = undefined; + } + return { + name: this.get("seller_name"), + email: email + }; + }, + + getOrder: function() { + var order = this.get("order"); + + if (order) { + return Ember.RSVP.resolve(order); + } else { + var orderDescription = this.get("order_description"); + var seller = Customer.create(this.getSellerCustomerAttributes()); + + return seller.save() + .then(function(seller) { + var description = orderDescription; + return seller.createOrder(description); + }); + } + }, +}); + +export default DebitOrderFactory; diff --git a/app/models/factories/hold-existing-funding-instrument-transaction-factory.js b/app/models/factories/hold-existing-funding-instrument-transaction-factory.js index aa9b5ad08..5a1b09f8d 100644 --- a/app/models/factories/hold-existing-funding-instrument-transaction-factory.js +++ b/app/models/factories/hold-existing-funding-instrument-transaction-factory.js @@ -1,38 +1,120 @@ import TransactionFactory from "./transaction-factory"; import ValidationHelpers from "balanced-dashboard/utils/validation-helpers"; +import Hold from "../hold"; +import Customer from "../customer"; var HoldExistingFundingInstrumentTransactionFactory = TransactionFactory.extend({ - source_uri: Ember.computed.readOnly("source.uri"), - getHoldAttributes: function() { - var properties = this.getProperties("amount", "description", "source_uri"); - properties.uri = this.get("source.card_holds_uri"); - return properties; - }, - validations: { dollar_amount: ValidationHelpers.positiveDollarAmount }, save: function() { - var Hold = BalancedApp.__container__.lookupFactory("model:hold"); + var self = this; + var order; var deferred = Ember.RSVP.defer(); - - var baseHoldAttributes = this.getHoldAttributes(); this.validate(); + + var getErrorMessage = function(error) { + return Ember.isBlank(error.additional) ? + error.description : + error.additional; + }; + if (this.get("isValid")) { - Hold.create(this.getHoldAttributes()) - .save() - .then(function(model) { - deferred.resolve(model); - }, function() { - deferred.reject(); + this.createOrderWithSeller() + .then(function(o) { + order = o; + var source = self.get("source"); + return self.attachCustomerToSource(source); + }) + .then(function(source) { + return self.createHold(source, order); + }) + .then(function(debit) { + deferred.resolve(debit); + }) + .catch(function(response) { + response.errors.forEach(function(error) { + if (error.extras) { + _.each(error.extras, function(value, key) { + self.get("validationErrors").add(key, "server", null, value); + }); + } + self.get("validationErrors").add(undefined, "server", null, getErrorMessage(error)); + }); + deferred.reject(self); }); } else { deferred.reject(); } return deferred.promise; - } + }, + + getBuyerCustomerAttributes: function() { + var email = this.get("buyer_email_address"); + if (Ember.isBlank(email)) { + email = undefined; + } + return { + name: this.get("buyer_name"), + email: email + }; + }, + + attachCustomerToSource: function(source) { + var customer = this.get("customer"); + + if (customer) { + return Ember.RSVP.resolve(source); + } else { + customer = Customer.create(this.getBuyerCustomerAttributes()).save(); + source.set('links.customer', customer.get("id")); + return source.save(); + } + }, + + getHoldAttributes: function() { + var properties = this.getProperties("amount", "description"); + return properties; + }, + + createHold: function(source, order) { + var sourceUri = source.get("uri"); + var holdsUri = source.get("card_holds_uri"); + var buyerUri = source.get("customer_uri"); + + var holdAttributes = _.extend({}, this.getHoldAttributes(), { + customer_uri: buyerUri, + uri: holdsUri, + source_uri: sourceUri, + order_uri: order.get("uri") + }); + + return Hold.create(holdAttributes).save(); + }, + + getSellerCustomerAttributes: function() { + var email = this.get("seller_email_address"); + if (Ember.isBlank(email)) { + email = undefined; + } + return { + name: this.get("seller_name"), + email: email + }; + }, + + createOrderWithSeller: function() { + var orderDescription = this.get("order_description"); + var seller = Customer.create(this.getSellerCustomerAttributes()); + + return seller.save() + .then(function(seller) { + var description = orderDescription; + return seller.createOrder(description); + }); + }, }); export default HoldExistingFundingInstrumentTransactionFactory; diff --git a/app/models/hold.js b/app/models/hold.js index 4d9bbba59..aa72f4357 100644 --- a/app/models/hold.js +++ b/app/models/hold.js @@ -2,11 +2,13 @@ import Ember from "ember"; import Model from "./core/model"; import Computed from "balanced-dashboard/utils/computed"; import Transaction from "./transaction"; +import TransactionSerializer from "../serializers/transaction"; var Hold = Transaction.extend({ card: Model.belongsTo('card', 'funding-instrument'), source: Ember.computed.alias('card'), debit: Model.belongsTo('debit', 'debit'), + order: Model.belongsTo('order', 'order'), status: function() { var apiStatus = this.get("__json.status"); @@ -45,4 +47,8 @@ var Hold = Transaction.extend({ funding_instrument_type: Ember.computed.alias('card.type_name') }); +Hold.reopenClass({ + serializer: TransactionSerializer.create() +}); + export default Hold; diff --git a/app/models/marketplace.js b/app/models/marketplace.js index ea6c6b6eb..fc075a727 100644 --- a/app/models/marketplace.js +++ b/app/models/marketplace.js @@ -85,19 +85,6 @@ var Marketplace = UserMarketplace.extend({ }); }, - search: function(query, resultsType, params) { - var baseUri = this.get("uri") + "/search"; - var searchParams = _.extend({ - sortField: "created_at", - sortOrder: "desc", - limit: 10, - query: query - }, params); - - var resultsUri = Utils.applyUriFilters(baseUri, searchParams); - return SearchModelArray.newArrayLoadedFromUri(resultsUri, resultsType); - }, - has_debitable_bank_account: Ember.computed.readOnly('owner_customer.has_debitable_bank_account'), has_bank_account: Ember.computed.readOnly('owner_customer.has_bank_account'), diff --git a/app/models/order.js b/app/models/order.js index 30e4f9160..ea6b6b242 100644 --- a/app/models/order.js +++ b/app/models/order.js @@ -54,9 +54,12 @@ var Order = Model.extend({ status: function() { if (this.get("isOverdue")) { return "overdue"; + } else if (this.get('amount_escrowed') === 0 && this.get("credits_list.length") > 0) { + return "completed"; } - return null; - }.property("isOverdue"), + + return "active"; + }.property("isOverdue", "amount_escrowed", "credits_list.length"), isOverdue: function() { if (this.get('amount_escrowed') === 0) { diff --git a/app/models/refund.js b/app/models/refund.js index 87df9bf2d..1cddb8e41 100644 --- a/app/models/refund.js +++ b/app/models/refund.js @@ -3,6 +3,7 @@ import Model from "./core/model"; import Transaction from "./transaction"; var Refund = Transaction.extend({ + order: Model.belongsTo('order', 'order'), debit: Model.belongsTo('debit', 'debit'), dispute: Model.belongsTo('dispute', 'dispute'), diff --git a/app/models/results-loaders/accounts.js b/app/models/results-loaders/accounts.js new file mode 100644 index 000000000..04f9d0341 --- /dev/null +++ b/app/models/results-loaders/accounts.js @@ -0,0 +1,8 @@ +import BaseResultsLoader from "./base"; +import Account from "../account"; + +var AccountsResultsLoader = BaseResultsLoader.extend({ + resultsType: Account, +}); + +export default AccountsResultsLoader; diff --git a/app/models/results-loaders/buyer-transactions.js b/app/models/results-loaders/buyer-transactions.js new file mode 100644 index 000000000..a141206e7 --- /dev/null +++ b/app/models/results-loaders/buyer-transactions.js @@ -0,0 +1,23 @@ +import FilterableTransactionsResultsLoader from "./filterable-transactions"; +import ModelArray from "../core/model-array"; + +var BuyerTransactionsResultsLoader = FilterableTransactionsResultsLoader.extend({ + results: function() { + var self = this; + + var results = this.get("unfilteredResults").filter(function(transaction) { + return !self.get("merchantOrderUris").contains(transaction.get("order_uri")); + }); + return ModelArray.create({ + isLoaded: true, + content: results + }); + }.property("unfilteredResults.@each.order_uri", "merchantOrderUris"), + + merchantOrderUris: function() { + return this.get("ordersResultsLoader.results").mapBy("uri"); + }.property("ordersResultsLoader.results.@each.uri"), + +}); + +export default BuyerTransactionsResultsLoader; diff --git a/app/models/results-loaders/filterable-transactions.js b/app/models/results-loaders/filterable-transactions.js new file mode 100644 index 000000000..17a65dcbb --- /dev/null +++ b/app/models/results-loaders/filterable-transactions.js @@ -0,0 +1,31 @@ +import TransactionsResultsLoader from "./transactions"; +import ModelArray from "../core/model-array"; +import SearchModelArray from "../core/search-model-array"; + +var FilterableTransactionsResultsLoader = TransactionsResultsLoader.extend({ + unfilteredResults: function() { + var uri = this.get('resultsUri'); + var type = this.get('resultsType'); + + if (Ember.isBlank(uri)) { + return ModelArray.create({ + isLoaded: true + }); + } else { + var searchArray = SearchModelArray.newArrayLoadedFromUri(uri, type); + searchArray.setProperties({ + sortProperties: [this.get('sortField') || 'created_at'], + sortAscending: this.get('sortDirection') === 'asc', + isLoaded: true + }); + + return searchArray; + } + }.property("resultsUri", "resultsType", "sortField", "sortDirection"), + + results: function() { + Ember.assert("Implement #results and make it return a ModelArray", false); + }.property() +}); + +export default FilterableTransactionsResultsLoader; diff --git a/app/models/results-loaders/marketplace-search.js b/app/models/results-loaders/marketplace-search.js index 7382ed10b..48bbf51be 100644 --- a/app/models/results-loaders/marketplace-search.js +++ b/app/models/results-loaders/marketplace-search.js @@ -4,32 +4,35 @@ import Transaction from "../transaction"; import FundingInstrument from "../funding-instrument"; import Customer from "../customer"; import Order from "../order"; +import Account from "../account"; +import Settlement from "../settlement"; import SearchModelArray from "../core/search-model-array"; - -var TRANSACTION_TYPES = ["credit", "debit", "card_hold", "refund", "reversal"]; -var FUNDING_INSTRUMENT_TYPES = ["card", "bank_account"]; -var CUSTOMER_TYPES = ["customer"]; -var ORDER_TYPES = ["order"]; +import Constants from "balanced-dashboard/utils/constants"; var MarketplaceSearchResultsLoader = BaseResultsLoader.extend({ - searchType: "transaction", + searchType: "order_transaction", limit: null, type: function() { var mapping = { - "funding_instrument": FUNDING_INSTRUMENT_TYPES, - "customer": CUSTOMER_TYPES, - "order": ORDER_TYPES + "order_transaction": Constants.SEARCH.ORDER_TRANSACTION_TYPES, + "funding_instrument": Constants.SEARCH.FUNDING_INSTRUMENT_TYPES, + "customer": Constants.SEARCH.CUSTOMER_TYPES, + "order": Constants.SEARCH.ORDER_TYPES, + "account": Constants.SEARCH.ACCOUNT_TYPES, + "settlement": Constants.SEARCH.SETTLEMENT_TYPES, }; - return mapping[this.get("searchType")] || TRANSACTION_TYPES; + return mapping[this.get("searchType")] || Constants.SEARCH.SEARCH_TYPES; }.property("searchType"), resultsType: function() { var mapping = { "funding_instrument": FundingInstrument, "customer": Customer, - "order": Order + "order": Order, + "account": Account, + "settlement": Settlement }; return mapping[this.get("searchType")] || Transaction; }.property("searchType"), diff --git a/app/models/results-loaders/merchant-transactions.js b/app/models/results-loaders/merchant-transactions.js new file mode 100644 index 000000000..841f38a75 --- /dev/null +++ b/app/models/results-loaders/merchant-transactions.js @@ -0,0 +1,23 @@ +import FilterableTransactionsResultsLoader from "./filterable-transactions"; +import ModelArray from "../core/model-array"; + +var MerchantTransactionsResultsLoader = FilterableTransactionsResultsLoader.extend({ + results: function() { + var self = this; + + var results = this.get("unfilteredResults").filter(function(transaction) { + return self.get("merchantOrderUris").contains(transaction.get("order_uri")); + }); + return ModelArray.create({ + isLoaded: true, + content: results + }); + }.property("unfilteredResults.@each.order_uri", "merchantOrderUris"), + + merchantOrderUris: function() { + return this.get("ordersResultsLoader.results").mapBy("uri"); + }.property("ordersResultsLoader.results.@each.uri"), + +}); + +export default MerchantTransactionsResultsLoader; diff --git a/app/models/results-loaders/orders.js b/app/models/results-loaders/orders.js index bb0b30b11..beb4fd11a 100644 --- a/app/models/results-loaders/orders.js +++ b/app/models/results-loaders/orders.js @@ -11,13 +11,13 @@ var OrdersResultsLoader = BaseResultsLoader.extend({ queryStringBuilder.addValues({ limit: this.get("limit"), sort: this.get("sort"), - + type: this.get("typeFilters"), "created_at[>]": this.get("startTime"), "created_at[<]": this.get("endTime"), }); return queryStringBuilder.getQueryStringAttributes(); - }.property("sort", "startTime", "endTime", "limit") + }.property("sort", "startTime", "endTime", "limit", "typeFilters") }); export default OrdersResultsLoader; diff --git a/app/models/results-loaders/unsettled-transactions.js b/app/models/results-loaders/unsettled-transactions.js new file mode 100644 index 000000000..887f5dce2 --- /dev/null +++ b/app/models/results-loaders/unsettled-transactions.js @@ -0,0 +1,57 @@ +import FilterableTransactionsResultsLoader from "./filterable-transactions"; +import ModelArray from "../core/model-array"; +import SearchModelArray from "../core/search-model-array"; + +var UnsettledTransactionsResultsLoader = FilterableTransactionsResultsLoader.extend({ + unfilteredResults: function() { + var uri = this.get('resultsUri'); + var type = this.get('resultsType'); + + if (Ember.isBlank(uri)) { + return ModelArray.create({ + isLoaded: true + }); + } else { + var searchArray = SearchModelArray.newArrayLoadedFromUri(uri, type); + searchArray.setProperties({ + sortProperties: [this.get('sortField') || 'created_at'], + sortAscending: this.get('sortDirection') === 'asc', + isLoaded: true + }); + + return searchArray; + } + }.property("resultsUri", "resultsType", "sortField", "sortDirection"), + + results: function() { + var self = this; + + var results = this.get("unfilteredResults").filter(function(credit) { + return !self.get("settledTransactionIds").contains(credit.get("id")); + }); + return ModelArray.create({ + isLoaded: true, + content: results + }); + }.property("unfilteredResults.@each.id", "settledTransactionIds"), + + settledTransactionIds: function() { + return this.get("settledTransactions").mapBy("id"); + }.property("settledTransactions.@each.id"), + + settledTransactions: function() { + var self = this; + var settlements = this.get("settlementsResultsLoader.results"); + var settledTransactions = []; + + settlements.forEach(function(settlement) { + var promise = SearchModelArray.newArrayLoadedFromUri(settlement.get("credits_uri"), "credit"); + promise.then(function(credits) { + settledTransactions.pushObjects(credits.content); + }); + }); + return settledTransactions; + }.property("settlementsResultsLoader.results.length") +}); + +export default UnsettledTransactionsResultsLoader; diff --git a/app/models/reversal.js b/app/models/reversal.js index 271011162..1b4ab8371 100644 --- a/app/models/reversal.js +++ b/app/models/reversal.js @@ -3,6 +3,7 @@ import Transaction from "./transaction"; import Model from "./core/model"; var Reversal = Transaction.extend({ + order: Model.belongsTo('order', 'order'), credit: Model.belongsTo('credit', 'credit'), type_name: 'Reversal', diff --git a/app/models/settlement.coffee b/app/models/settlement.coffee new file mode 100644 index 000000000..ade299289 --- /dev/null +++ b/app/models/settlement.coffee @@ -0,0 +1,14 @@ +`import Ember from "ember";` +`import Utils from "balanced-dashboard/lib/utils";` +`import Model from "./core/model";` + +Settlement = Model.extend( + routeName: "settlement", + route_name: "settlement", + type_name: "Settlement", + amountInDollars: (-> + "$%@".fmt(Utils.centsToDollars(@get("amount"))) + ).property("amount") +) + +`export default Settlement;` diff --git a/app/models/settlement.js b/app/models/settlement.js deleted file mode 100644 index ce8e49a63..000000000 --- a/app/models/settlement.js +++ /dev/null @@ -1,10 +0,0 @@ -import Transaction from "./transaction"; -import Model from "./core/model"; - -var Settlement = Transaction.extend({ - debit: Model.belongsTo('debit', 'debit'), - type_name: 'settlement', - route_name: 'Settlement' -}); - -export default Settlement; diff --git a/app/router.coffee b/app/router.coffee index bf4cb65e8..5d26594ab 100644 --- a/app/router.coffee +++ b/app/router.coffee @@ -48,20 +48,31 @@ Router.map -> this.route('import_payouts') # exists to handle old URIs - this.resource('accounts', path: '/accounts/:item_id') this.route("redirect_activity_transactions", path: '/activity/transactions') + this.route("redirect_transactions", path: '/transactions') this.route("redirect_activity_orders", path: '/activity/orders') this.route("redirect_activity_customers", path: 'activity/customers') this.route("redirect_activity_funding_instruments", path: 'activity/funding_instruments') this.route("redirect_activity_disputes", path: 'activity/disputes') this.route("redirect_invoices", path: 'invoices') + this.resource('account', path: '/accounts/:item_id') + this.route("orders") this.resource('orders', path: '/orders/:item_id') + this.resource('credits', path: '/credits/:item_id') + this.resource('reversals', path: '/reversals/:item_id') + this.resource('debits', path: '/debits/:item_id') + this.resource('holds', path: '/holds/:item_id') + this.resource('refunds', path: '/refunds/:item_id') + this.resource('events', path: '/events/:item_id') this.route("customers") this.resource('customer', path: '/customers/:item_id') + this.route("settlements") + this.resource('settlement', path: '/settlements/:item_id') + this.route("disputes") this.resource('dispute', path: '/disputes/:item_id') @@ -69,14 +80,6 @@ Router.map -> this.resource('bank_accounts', path: '/bank_accounts/:item_id') this.resource('cards', path: '/cards/:item_id') - this.route("transactions") - this.resource('credits', path: '/credits/:item_id') - this.resource('reversals', path: '/reversals/:item_id') - this.resource('debits', path: '/debits/:item_id') - this.resource('holds', path: '/holds/:item_id') - this.resource('refunds', path: '/refunds/:item_id') - this.resource('events', path: '/events/:item_id') - this.route("logs") this.resource("log", path: "/logs/:item_id") diff --git a/app/routes/account.coffee b/app/routes/account.coffee new file mode 100644 index 000000000..de90b81f0 --- /dev/null +++ b/app/routes/account.coffee @@ -0,0 +1,27 @@ +`import ModelRoute from "./model"` + +AccountRoute = ModelRoute.extend( + model: (params) -> + store = @getStore() + store.fetchItem("account", "/accounts/#{params.item_id}") + + setupController: (controller, model) -> + @_super(controller, model) + + this.get("container").lookupFactory("model:customer").find(model.get("customer_uri")).then (customer) -> + model.set("customer", customer) + + settlementsResultsLoader = this.get("container").lookupFactory("results-loader:transactions").create({ + path: model.get("settlements_uri") + }); + + unsettledCreditsResultsLoader = this.get("container").lookupFactory("results-loader:unsettled_transactions").create({ + path: model.get("credits_uri"), + settlementsResultsLoader: settlementsResultsLoader + }); + + controller.set("creditsResultsLoader", unsettledCreditsResultsLoader); + controller.set("settlementsResultsLoader", settlementsResultsLoader); +) + +`export default AccountRoute` diff --git a/app/routes/customer.js b/app/routes/customer.js index 38f77a52f..b0223027a 100644 --- a/app/routes/customer.js +++ b/app/routes/customer.js @@ -1,5 +1,6 @@ import ModelRoute from "./model"; import Customer from "../models/customer"; +import LegacyResultsLoaderWrapper from "balanced-dashboard/utils/legacy-results-loader-wrapper"; var CustomerRoute = ModelRoute.extend({ title: 'Customer', @@ -8,6 +9,18 @@ var CustomerRoute = ModelRoute.extend({ setupController: function(controller, customer) { this._super(controller, customer); + var store = this.getStore(); + store.fetchCollection("account", customer.get("accounts_uri"), { limit: 10 }).then(function(collection) { + var wrapper = LegacyResultsLoaderWrapper.create({collection: collection}); + controller.set("accountsResultsLoader", wrapper); + }); + + var ordersResultsLoader = customer.getOrdersLoader({ + limit: 10 + }); + + controller.set("ordersResultsLoader", ordersResultsLoader); + controller.setProperties({ fundingInstrumentsResultsLoader: customer.getFundingInstrumentsLoader({ limit: 10 @@ -18,6 +31,16 @@ var CustomerRoute = ModelRoute.extend({ transactionsResultsLoader: customer.getTransactionsLoader({ limit: 10, status: ['pending', 'succeeded', 'failed'] + }), + buyerTransactionsResultsLoader: customer.getBuyerTransactionsLoader({ + limit: 10, + status: ['pending', 'succeeded', 'failed'], + ordersResultsLoader: ordersResultsLoader + }), + merchantTransactionsResultsLoader: customer.getMerchantTransactionsLoader({ + limit: 10, + status: ['pending', 'succeeded', 'failed'], + ordersResultsLoader: ordersResultsLoader }) }); } diff --git a/app/routes/marketplace/import-payouts.js b/app/routes/marketplace/import-payouts.js index f9dac529e..6d9006117 100644 --- a/app/routes/marketplace/import-payouts.js +++ b/app/routes/marketplace/import-payouts.js @@ -1,7 +1,7 @@ import AuthRoute from "../auth"; var MarketplaceImportPayoutsRoute = AuthRoute.extend({ - pageTitle: 'Import payouts', + pageTitle: 'Import one-off credits', setupController: function(controller, model) { controller.refresh(); diff --git a/app/routes/marketplace/index.js b/app/routes/marketplace/index.js index 48100c297..1bbbc1d12 100644 --- a/app/routes/marketplace/index.js +++ b/app/routes/marketplace/index.js @@ -2,13 +2,7 @@ import AuthRoute from "balanced-dashboard/routes/auth"; var MarketplaceIndexRoute = AuthRoute.extend({ beforeModel: function() { - var mp = this.modelFor("marketplace"); - if (mp.get("isOrdersRequired")) { - this.transitionTo('marketplace.orders'); - } - else { - this.transitionTo('marketplace.transactions'); - } + this.transitionTo('marketplace.orders'); } }); diff --git a/app/routes/marketplace/orders.js b/app/routes/marketplace/orders.js index 332543ef8..4ad519563 100644 --- a/app/routes/marketplace/orders.js +++ b/app/routes/marketplace/orders.js @@ -4,7 +4,7 @@ var MarketplaceOrdersRoute = AuthRoute.extend({ pageTitle: 'Orders', model: function() { var marketplace = this.modelFor("marketplace"); - return marketplace.getOrdersLoader(); + return marketplace.getTransactionsLoader(); }, }); diff --git a/app/routes/marketplace/settlements.js b/app/routes/marketplace/settlements.js new file mode 100644 index 000000000..e6f3282b4 --- /dev/null +++ b/app/routes/marketplace/settlements.js @@ -0,0 +1,15 @@ +import AuthRoute from "../auth"; +import LegacyResultsLoaderWrapper from "balanced-dashboard/utils/legacy-results-loader-wrapper"; + +var MarketplaceSettlementsRoute = AuthRoute.extend({ + pageTitle: 'Settlements', + model: function() { + var store = this.container.lookup("controller:marketplace").get("store"); + return store.fetchCollection("settlement", "/settlements", { limit: 50 }).then(function(collection) { + var wrapper = LegacyResultsLoaderWrapper.create({collection: collection}); + return wrapper; + }); + }, +}); + +export default MarketplaceSettlementsRoute; diff --git a/app/routes/marketplace/transactions.js b/app/routes/marketplace/transactions.js deleted file mode 100644 index 770e87255..000000000 --- a/app/routes/marketplace/transactions.js +++ /dev/null @@ -1,11 +0,0 @@ -import AuthRoute from "../auth"; - -var MarketplaceTransactionsRoute = AuthRoute.extend({ - pageTitle: 'Transactions', - model: function() { - var marketplace = this.modelFor("marketplace"); - return marketplace.getTransactionsLoader(); - }, -}); - -export default MarketplaceTransactionsRoute; diff --git a/app/routes/model.js b/app/routes/model.js index a98b1a410..8652636cf 100644 --- a/app/routes/model.js +++ b/app/routes/model.js @@ -2,6 +2,10 @@ import TitleRoute from "./title"; import Utils from "balanced-dashboard/lib/utils"; var ModelRoute = TitleRoute.extend({ + getStore: function() { + return this.get("container").lookup("controller:marketplace").get("store"); + }, + model: function(params) { var marketplace = this.modelFor('marketplace'); var modelObject = this.get('modelObject'); diff --git a/app/routes/settlement.coffee b/app/routes/settlement.coffee new file mode 100644 index 000000000..fafe8e95f --- /dev/null +++ b/app/routes/settlement.coffee @@ -0,0 +1,26 @@ +`import ModelRoute from "./model";` +`import Settlement from "../models/bk/settlement";` + +SettlementRoute = ModelRoute.extend( + model: (params) -> + store = @getStore() + store.fetchItem("settlement", "/settlements/#{params.item_id}") + + setupController: (controller, model) -> + @_super(controller, model) + + store = @getStore() + store.fetchItem("account", model.get("source_uri")).then (source) -> + model.set("source", source.toLegacyModel()) + + store.fetchItem("account", model.get("destination_uri")).then (destination) -> + model.set("destination", destination.toLegacyModel()) + + creditsResultsLoader = this.get("container").lookupFactory("results-loader:transactions").create({ + path: model.get("credits_uri") + }); + + controller.set("creditsResultsLoader", creditsResultsLoader); +) + +`export default SettlementRoute;` diff --git a/app/serializers/transaction.js b/app/serializers/transaction.js new file mode 100644 index 000000000..4c27728e0 --- /dev/null +++ b/app/serializers/transaction.js @@ -0,0 +1,17 @@ +import Rev1Serializer from "./rev1"; + +var TransactionSerializer = Rev1Serializer.extend({ + _propertiesMap: function(record) { + var json = this._super(record); + + if (!Ember.isBlank(json.order_uri)) { + json.order = json.order_uri; + delete json.order_uri; + } + + return json; + } +}); + + +export default TransactionSerializer; diff --git a/app/stores/balanced.coffee b/app/stores/balanced.coffee index 0b9d7486f..5ab873a8e 100644 --- a/app/stores/balanced.coffee +++ b/app/stores/balanced.coffee @@ -4,6 +4,8 @@ BalancedStore = Store.extend( modelMaps: bank_account: "model:bk/bank-account" customer: "model:bk/customer" + account: "model:bk/account" + settlement: "model:bk/settlement" api_key_production: "model:bk/api-key-production" marketplace: "model:bk/marketplace" ) diff --git a/app/styles/components.less b/app/styles/components.less index 081edf01a..c71057a13 100644 --- a/app/styles/components.less +++ b/app/styles/components.less @@ -47,3 +47,15 @@ } } } + +.dropdown-toggle.ellipsis { + min-width: 0; + margin-right: 0; + padding: 4px 8px; + + span { + .sl-16-b; + color: @gray4; + line-height: 20px; + } +} diff --git a/app/styles/form_group.less b/app/styles/form_group.less index 89864d0fb..3c9584657 100644 --- a/app/styles/form_group.less +++ b/app/styles/form_group.less @@ -30,6 +30,10 @@ p { .sl-sm; + + a { + font-size: 12px; + } } .key-value-field-group { diff --git a/app/styles/login.less b/app/styles/login.less index 798859cec..a420cc591 100644 --- a/app/styles/login.less +++ b/app/styles/login.less @@ -37,6 +37,33 @@ } } +.intro-page { + padding: 100px 200px; + text-align: center; + + i.non-interactive { + color: @gray2; + font-size: 60px; + } + + h2 { + .sl-lg-sb; + padding: 20px 0; + margin: 20px 0 0; + } + + p.description { + .sl-20; + color: @gray6; + width: 650px; + margin: 0 auto 10px; + } + + div.center { + margin: 30px 0 25px; + } +} + .page-form { background-color: @gray0; border: 1px solid @gray2; diff --git a/app/styles/marketplace.less b/app/styles/marketplace.less index 9db8ce92a..d26231d57 100644 --- a/app/styles/marketplace.less +++ b/app/styles/marketplace.less @@ -112,8 +112,15 @@ form { li { margin: 5px 0; position: relative; - .delete { - float: right; + + .icon-table-x { + color: @gray1; + margin-top: 25px; + margin-right: 10px; + + &:hover { + color: @white; + } } .info { height: 65px; diff --git a/app/styles/pages.less b/app/styles/pages.less index c1132b0d1..c4c69a94f 100644 --- a/app/styles/pages.less +++ b/app/styles/pages.less @@ -173,6 +173,16 @@ } } + &.completed { + span { + color: @gray4; + } + + &:before { + background-color: @gray3; + } + } + &.overdue { &:before { font-family: 'Balanced-Icon'; @@ -287,6 +297,10 @@ section[class$="-info"] { color: @gray8; } +.drawer-open { + display: block; +} + .dispute-alert { margin: -45px -45px 45px -45px; diff --git a/app/styles/popovers.less b/app/styles/popovers.less index 6b2a0dd0f..ab95cdcc6 100644 --- a/app/styles/popovers.less +++ b/app/styles/popovers.less @@ -1,3 +1,7 @@ +[data-toggle="popover"] { + display: inline-block; +} + .icon-tooltip { font-size: 16px; margin-left: 3px; diff --git a/app/styles/results.less b/app/styles/results.less index 06499d479..422a25d9e 100644 --- a/app/styles/results.less +++ b/app/styles/results.less @@ -116,6 +116,11 @@ header.results-label h3 { } } + i.non-interactive { + color: @black; + margin-right: 5px; + } + td.unlinked-status { i.icon-unlinked { color: @imperialRed60; @@ -140,13 +145,78 @@ header.results-label h3 { } } + td.ungrouped-transactions-container { + padding-left: 0; + padding-right: 0; + + table.grouped-transactions { + border-bottom: none; + + thead { + border: none; + margin-top: -1px; + + th { + border-top: none; + border-bottom: none; + } + } + + tr { + &:last-of-type { + border-bottom: none; + } + + &:hover { + background-color: @persianBlue10; + } + } + } + } + td.grouped-transactions-container { padding-left: 0; padding-right: 0; + &.nested { + table.grouped-transactions:first-of-type { + border-bottom: none; + + td:first-of-type { + padding-left: 25px; + } + } + + table.grouped-transactions { + border-bottom: none; + + td:first-of-type { + padding-left: 50px; + } + } + } + + &.divided { + table.grouped-transactions { + td:first-of-type { + padding-left: 10px; + } + } + } + table.grouped-transactions { border-bottom: 1px solid @gray2; + thead { + border: none; + margin-top: -1px; + + th { + border-top: none; + border-bottom: none; + } + } + &:last-of-type { border-bottom: none; } @@ -168,6 +238,7 @@ header.results-label h3 { } } } + td { a.active { pointer-events: none; @@ -178,70 +249,31 @@ header.results-label h3 { } } + &:first-of-type { + padding-left: 25px; + } + &.status { text-transform: none; padding-left: 10px; - &:first-of-type { - padding-left: 25px; - } - - a > span > .secondary { - margin-left: 14px; - } - - &.orders .order { + & > a > span { &:before { - font-family: 'Balanced-Icon'; - background-color: transparent; - &:extend(.icon-orders:before); - font-size: 10px; - color: @gray4; - margin-top: -14px; - } - > .primary { - margin-left: 14px; - } - } - - &.dispute > a > span { - &:before { - font-family: 'Balanced-Icon'; - background-color: transparent; - &:extend(.icon-disputes:before); - font-size: 12px; - color: @curryYellow80; - margin-top: -12px; - } - - &.new:before { - color: @curryYellow80; - } - - &.under_review:before { - color: @byzantiumPurple80; - } - - &.won:before { - color: @pineGreen80; - } - - &.lost:before { - color: @imperialRed80; + margin-left: 2px; } - > .primary { - margin-left: 14px; + & > .primary, & > .secondary { + margin-left: 16px; } } - a > span:after { + &.connected a > span:after { position: absolute; content: ''; border-left: 2px solid @gray3; height: 30px; margin-top: -19px; - margin-left: 3px; + margin-left: 5px; } } } @@ -249,6 +281,17 @@ header.results-label h3 { } } + td.payment-method { + [class^="icon-"] { + font-family: 'Balanced-Icon'; + color: @gray4; + } + + & > a > span > .secondary { + margin-left: 19px; + } + } + td.status { a > span:before { content: ''; @@ -263,11 +306,34 @@ header.results-label h3 { a > span:before { float: left; margin-top: 7px; + margin-bottom: 10px; } + } - a > span.failed:before { - float: left; - margin-top: 14px; + &.dispute > a > span { + &:before { + font-family: 'Balanced-Icon'; + background-color: transparent; + &:extend(.icon-disputes:before); + font-size: 12px; + color: @curryYellow80; + margin-top: -12px; + } + + &.new:before { + color: @curryYellow80; + } + + &.under_review:before { + color: @byzantiumPurple80; + } + + &.won:before { + color: @pineGreen80; + } + + &.lost:before { + color: @imperialRed80; } } @@ -283,7 +349,7 @@ header.results-label h3 { } } - .failed, .expired, .lost, .arbitration, .canceled, .voided, .invalidated, .removed, .bad { + .failed, .expired, .lost, .arbitration, .canceled, .voided, .invalidated, .removed, .overdue, .bad { &:before { background-color: @imperialRed80; } @@ -292,6 +358,10 @@ header.results-label h3 { .under_review:before { background-color: @byzantiumPurple80; } + + .completed:before { + background-color: @gray3; + } } .num, .align-right { diff --git a/app/styles/scaffolding.less b/app/styles/scaffolding.less index 30f983f68..dcc999f62 100644 --- a/app/styles/scaffolding.less +++ b/app/styles/scaffolding.less @@ -121,12 +121,6 @@ min-width: 90px; } -#order-sort-menu { - .caret { - margin-top: 11px; - } -} - .dropdown-menu.align-right { right: 0; left: auto; @@ -168,6 +162,12 @@ hr { padding: 1px 8px 0; margin-top: 3px; .border-radius(15px); + + &.new { + background-color: @curryYellow60; + font-size: 10px; + text-transform: uppercase; + } } .payments-navbar { diff --git a/app/templates/account.hbs b/app/templates/account.hbs new file mode 100644 index 000000000..df665cbf9 --- /dev/null +++ b/app/templates/account.hbs @@ -0,0 +1,23 @@ +{{view "page-navigations/page-navigation" pageType=model.type_name title=model.id}} + +{{#view "detail-views/body-panel"}} + {{#view "detail-views/api-model-panel" model=model}} +
+ {{view detail-views/summary-sections/account-summary-section model=model}} + {{view detail-views/description-lists/account-titled-key-values-section model=model}} + {{view "meta-list" type=model}} + {{/view}} + + {{#view "detail-views/main-panel"}} ++ {{view.description}} + {{#if view.isLearnMoreText}} + Learn more + {{/if}} +
+{{/if}} +{{#if view.isHasButton}} + + {{view.buttonModalText}} + +{{/if}} + + +{{#if view.isLearnMoreText}} ++ {{view.learnMoreText}} +
+{{/if}} diff --git a/app/templates/detail-views/summary-items/label-base.hbs b/app/templates/detail-views/summary-items/label-base.hbs new file mode 100644 index 000000000..c84cf5809 --- /dev/null +++ b/app/templates/detail-views/summary-items/label-base.hbs @@ -0,0 +1,7 @@ + {{view.text}} + +{{#if view.isModalEditLink}} + + + +{{/if}} diff --git a/app/templates/detail-views/summary-section-base.hbs b/app/templates/detail-views/summary-section-base.hbs new file mode 100644 index 000000000..8984fefdf --- /dev/null +++ b/app/templates/detail-views/summary-section-base.hbs @@ -0,0 +1,5 @@ +- {{view.statusText}} - - {{#if view.learnMore}} - Learn more - {{/if}} -
- {{/if}} - - {{#if view.statusButtonModalView}} - - {{view.statusButtonText}} - - {{/if}} - - {{#if view.learnMore}} -- {{view.learnMore.text}} -
- {{/if}} -