jam-cloud/web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen...

1123 lines
38 KiB
CoffeeScript

context = window
rest = context.JK.Rest()
logger = context.JK.logger
LocationActions = context.LocationActions
AppStore = context.AppStore
UserStore = context.UserStore
SubscriptionActions = context.SubscriptionActions
profileUtils = context.JK.ProfileUtils
@AccountPaymentHistoryScreen = React.createClass({
mixins: [
ICheckMixin,
Reflux.listenTo(AppStore, "onAppInit"),
Reflux.listenTo(UserStore, "onUserChanged"),
Reflux.listenTo(@LocationStore, "onLocationsChanged"),
Reflux.listenTo(SubscriptionStore, "onSubscriptionChanged")
]
shownOnce: false
screenVisible: false
LIMIT: 20
TILE_PAYMENTS_TO_YOU: 'payments to you'
TILE_PAYMENTS_TO_JAMKAZAM: 'payments to jamkazam'
TILE_PAYMENT_METHOD: 'payment method'
STUDENT_TILES: ['payment method', 'payments to jamkazam']
TEACHER_TILES: ['payments to jamkazam', 'payments to you']
onAppInit: (@app) ->
@app.bindScreen('account/paymentHistory', {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide})
onUserChanged: (userState) ->
if !@shouldShowNameSet
@shouldShowNameSet = true
if userState?.user?
username = userState.user.name
first_name = userState.user.first_name
last_name = userState.user.last_name
shouldShowName = !username? || username.trim() == '' || username.toLowerCase().indexOf('anonymous') > -1
else
shouldShowName = @state.shouldShowName
if userState?.user?.teacher?
@setState({selected: 'payments to you'})
@setState({user: userState?.user, shouldShowName: shouldShowName})
componentDidMount: () ->
#@checkboxes = [{selector: 'input.billing-address-in-us', stateKey: 'billingInUS'}]
@root = $(@getDOMNode())
@endOfList = @root.find('.end-of-payments-list')
@contentBodyScroller = @root
#@iCheckify()
@preparePaymentMethod()
componentDidUpdate: (prevProps, prevState) ->
#@iCheckify()
#$expiration = @root.find('input.expiration')
#if !$expiration.data('payment-applied')
# $expiration.payment('formatCardExpiry').data('payment-applied', true)
#$cardNumber = @root.find("input.card-number")
#if !$cardNumber.data('payment-applied')
# $cardNumber.payment('formatCardNumber').data('payment-applied', true)
#$cvv = @root.find("input.cvv")
#if !$cvv.data('payment-applied')
# $cvv.payment('formatCardCVC').data('payment-applied', true)
if @currentNext() == null
@contentBodyScroller.off('scroll')
if @state[@getCurrentPageName()] == 1 and @getCurrentList().length == 0
@endOfList.show()
logger.debug("PaymentHistoryScreen: empty search")
else if @state[@getCurrentPageName()] > 0
logger.debug("end of search")
@endOfList.show()
else
@registerInfiniteScroll(@contentBodyScroller)
if @activeTile(prevState.selected) != @activeTile() && @getCurrentList().length == 0
@refresh()
@configureElements()
registerInfiniteScroll:() ->
$scroller = @contentBodyScroller
logger.debug("registering infinite scroll")
$scroller.off('scroll')
$scroller.on('scroll', () =>
# be sure to not fire off many refreshes when user hits the bottom
return if @refreshing
if $scroller.scrollTop() + $scroller.innerHeight() + 100 >= $scroller[0].scrollHeight
#$scroller.append('<div class="infinite-scroll-loader-2">... Loading more Payments ...</div>')
@setState({searching: true})
logger.debug("refreshing more payments for infinite scroll")
@nextPage()
)
nextPage: () ->
#nextPage = @state.salesCurrentPage + 1
@incrementCurrentPage()
@refresh()
checkboxChanged: (e) ->
checked = $(e.target).is(':checked')
value = $(e.target).val()
@setState({userSchedulingComm: value})
beforeHide: (e) ->
@screenVisible = false
@resetErrors()
beforeShow: (e) ->
afterShow: (e) ->
@clearResults()
@screenVisible = true
@refresh()
#@getUncollectables()
resetErrors: () ->
@setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null, nameError: null, firstNameError: null, lastNameError: null, cityError: null, stateError: null, address1Error: null, countryError: null, address2Error:null})
onBack: (e) ->
e.preventDefault()
window.history.go(-1);
checkboxChanged: (e) ->
checked = $(e.target).is(':checked')
@setState({billingInUS: checked})
refresh: () ->
@buildQuery()
if @activeTile() == @TILE_PAYMENTS_TO_YOU
@refreshTeacherDistributions()
else if @activeTile() == @TILE_PAYMENTS_TO_JAMKAZAM
@refreshSales()
else if @activeTile() == @TILE_PAYMENT_METHOD
@refreshPayment()
else
logger.debug("dropping refresh because no tile match", @activeTile)
refreshPayment:() ->
SubscriptionActions.updateSubscription()
refreshSales: () ->
@refreshing = true
#rest.getSalesHistory(@currentQuery)
#.done(@salesHistoryDone)
#.fail(@salesHistoryFail)
rest.listInvoices()
.done(@paymentHistoryDone)
.fail(@paymentHistoryFail)
refreshTeacherDistributions: () ->
@refreshing = true
rest.listTeacherDistributions(@currentQuery)
.done(@teacherDistributionsDone)
.fail(@teacherDistributionsFail)
getUncollectables: () ->
rest.getUncollectables({})
.done(@uncollectablesDone)
.fail(@uncollectablesFail)
salesHistoryDone:(response) ->
@refreshing = false
this.setState({salesNext: response.next, sales: this.state.sales.concat(response.entries)})
salesHistoryFail:(jqXHR) ->
@refreshing = false
@app.notifyServerError jqXHR, 'Payments to JamKazam Unavailable'
paymentHistoryDone:(response) ->
@refreshing = false
this.setState({salesNext: null, sales: this.state.sales.concat(response.entries), pastDue: response.past_due})
paymentHistoryFail:(jqXHR) ->
@refreshing = false
@app.notifyServerError jqXHR, 'Payments to JamKazam Unavailable'
teacherDistributionsDone:(response) ->
@refreshing = false
this.setState({distributionsNext: response.next, distributions: this.state.distributions.concat(response.entries)})
teacherDistributionsFail:(jqXHR) ->
@refreshing = false
@app.notifyServerError jqXHR, 'Payments to You Unavailable'
uncollectablesDone: (response) ->
this.setState({uncollectables: response})
uncollectablesFail: (jqXHR) ->
@app.notifyServerError jqXHR, 'Unable to fetch uncollectable info'
clearResults:() ->
this.setState({salesCurrentPage: 0, sales: [], distributionsCurrentPage: 0, distributions: [], salesNext: null, distributionsNext: null, updating: false, pastDue: false})
buildQuery:(page = @getCurrentPage()) ->
@currentQuery = this.defaultQuery(page)
defaultQuery:(page = @getCurrentPage()) ->
query =
per_page: @LIMIT
page: page + 1
if @currentNext()
query.page = @currentNext()
query
getCurrentPage: () ->
page = this.state[@getCurrentPageName()]
if !page?
page = 1
page
incrementCurrentPage: () ->
newState = {}
newState[@getCurrentPageName] = @state[@getCurrentPageName()] + 1
this.setState(newState)
getCurrentPageName: () ->
if @activeTile() == @TILE_PAYMENTS_TO_JAMKAZAM
'salesCurrentPage'
else if @activeTile() == @TILE_PAYMENTS_TO_YOU
'distributionsCurrentPage'
else
1
getCurrentList: () ->
if @activeTile() == @TILE_PAYMENTS_TO_JAMKAZAM
@state['sales']
else if @activeTile() == @TILE_PAYMENTS_TO_YOU
@state['distributions']
else
@state['sales']
currentNext: () ->
if @activeTile() == @TILE_PAYMENTS_TO_JAMKAZAM
@state.salesNext
else if @activeTile() == @TILE_PAYMENTS_TO_YOU
@state.distributionsNext
else
null
onClick: (e) ->
e.preventDefault()
context.location.href = '/client#/account'
getInitialState: () ->
{
user: null,
nextPager: null,
salesCurrentPage: 0,
distributionsCurrentPage: 0
salesNext: null,
distributionsNext: null
sales: [],
distributions: []
selected: @TILE_PAYMENT_METHOD,
updating: false,
billingInUS: true,
userWantsUpdateCC: false,
selectedCountry: null,
paypal_updating: false,
uncollectables: []
}
onCancel: (e) ->
e.preventDefault()
context.location.href = '/client#/account'
mainContent: () ->
if !@state.user?
`<div className="loading">Loading...</div>`
else if @state.selected == @TILE_PAYMENTS_TO_YOU
@paymentsToYou()
else if @state.selected == @TILE_PAYMENTS_TO_JAMKAZAM
@paymentsToJamKazam()
else if @state.selected == @TILE_PAYMENT_METHOD
@paymentMethod()
else
@paymentsToJamKazam()
paymentsToYou: () ->
rows = []
for paymentHistory in @getCurrentList()
paymentMethod = 'Stripe'
if paymentHistory.distributed
date = paymentHistory.teacher_payment.teacher_payment_charge.last_billing_attempt_at
status = 'Paid'
else
date = paymentHistory.created_at
if paymentHistory.not_collectable
status = 'Uncollectible'
else if !paymentHistory.teacher?.teacher?.stripe_account_id?
status = 'No Stripe Acct'
else
status = 'Collecting'
date = context.JK.formatDate(date, true)
description = paymentHistory.description
if paymentHistory.teacher_payment?
amt = paymentHistory.teacher_payment.real_distribution_in_cents
else
amt = paymentHistory.real_distribution_in_cents
displayAmount = ' $' + (amt/100).toFixed(2)
amountClasses = {status: status}
row =
`<tr>
<td>{date}</td>
<td className="capitalize">{paymentMethod}</td>
<td>{description}</td>
<td className="capitalize">{status}</td>
<td className={classNames(amountClasses)}>{displayAmount}</td>
</tr>`
rows.push(row)
`<div>
<table className="payment-table">
<thead>
<tr>
<th>DATE</th>
<th>METHOD</th>
<th>DESCRIPTION</th>
<th>STATUS</th>
<th>AMOUNT</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<a className="btn-next-pager" href="/api/sales?page=1">Next</a>
<div className="end-of-payments-list end-of-list">No more payment history</div>
<div className="input-aligner">
<a className="back button-grey" onClick={this.onBack}>BACK</a>
</div>
<br className="clearall" />
</div>`
onSubscriptionChanged: (subscription) ->
@setState({pastDue: subscription.past_due})
onLocationsChanged: (countries) ->
console.log("countries in ", countries)
@setState({countries: countries})
onCountryChanged: (e) ->
val = $(e.target).val()
@setState({selectedCountry: val})
currentCountry: () ->
this.state.selectedCountry || this.props.selectedCountry || ''
openBrowser: () ->
context.JK.popExternalLink("https://www.jamkazam.com/client#/subscription")
onRecurlyToken: (err, token_data) ->
if err
console.log("error", err)
handled = false
if err.code == "validation" && err.fields?
handled = true
for field in err.fields
console.log("problem field", field)
# handle error using err.code and err.fields
if field == "first_name"
@setState({firstNameError: true})
if field == "last_name"
@setState({lastNameError: true})
if err.code == "invalid-parameter"
if err.fields.indexOf("year") > -1 || err.fields.indexOf("month") > -1 || err.fields.indexOf("number") > -1 || err.fields.indexOf("cvv") > -1
@setState({ccError: true})
handled = true
@app.layout.notify({title: 'Please double-check ' + err.fields[0], text: err.message})
if !handled
@app.layout.notify({title: "Error Updating Payment Info", text: JSON.stringify(err)})
@setState({updating: false})
else
# recurly.js has filled in the 'token' field, so now we can submit the
# form to your server
console.log("eintercepted", token_data)
rest.updatePayment({recurly_token: token_data.id}).done((response) => @updatePaymentDone(response)).fail((jqXHR) => @updatePaymentFailure(jqXHR))
updatePaymentDone: (response) ->
@setState({updating: false, "paypal_updating": false})
logger.debug("recurly submitted: " + JSON.stringify(response))
@setState({userWantsUpdateCC: false})
#if @state.shouldShowName
window.UserActions.refresh()
if response.uncollectables
context.JK.Banner.showAlert('Credit Card Updated', 'Than you. Your credit card info has been updated.<br/><br/>We will try to bill any unpaid lessons within the next hour, and an email will be sent at that time.')
else
@app.layout.notify({title: 'Payment Updated', text: 'Your payment info has been updated.'})
updatePaymentFailure: (jqXHR) ->
@setState({updating: false, "paypal_updating": false})
handled = false
if jqXHR.status == 404
errors = JSON.parse(jqXHR.responseText)?.message
@app.layout.notify({title: "Error Updating Payment Info", text: errors})
else
@app.notifyServerError(jqXHR, 'Payment Not Updated')
onFormSubmit: (event) ->
form = event.target
console.log("ok work this", form)
event.preventDefault()
@setState({updating: true})
recurly.token(@elements, form, @onRecurlyToken)
configureRecurly: () ->
if !window.configuredRecurly
console.log("configuring recurly...")
window.recurly.configure(gon.global.recurly_public_api_key)
window.configuredRecurly = true
@setState({"configuredRecurly": true})
@elements = window.recurly.Elements()
# then load paypal:
@paypal = window.recurly.PayPal({ braintree: { clientAuthorization: gon.global.braintree_token} })
@paypal.on('error', this.onPayPalError)
@paypal.on('token', this.onPayPalToken)
delayedConfigure: () ->
if gon.isNativeClient
return
if !window.recurly?
console.log("relaunch delayed recurly configure")
setTimeout(() =>
@delayedConfigure()
, 1000)
return
@configureRecurly()
@configureElements()
configureElements: () ->
node = $('#subscription-elements')
if node.length > 0
if window.recurly && @elements? && !node.data('recurlied')
commonStyles = {
inputType: 'mobileSelect',
style: {
fontSize:'1em'
color:'black',
placeholder: {
color:'black',
}
invalid: {
fontColor: 'red'
}
}
}
#cardNumberStyle = $.extend(true, {}, commonStyles, {style:{placeholder:{content: 'Card Number'}}})
#cardMonthStyle = $.extend(true, {}, commonStyles, {style:{placeholder:{content: 'MM'}}})
#cardYearStyle = $.extend(true, {}, commonStyles, {style:{placeholder:{content: 'YYYY'}}})
#cardCvcStyle = $.extend(true, {}, commonStyles, {style:{placeholder:{content: 'CVC'}}})
#cardNumberElement = @elements.CardNumberElement( cardNumberStyle )
#cardMonthElement = @elements.CardMonthElement(cardMonthStyle)
#cardYearElement = @elements.CardYearElement(cardYearStyle)
#cardCvvElement = @elements.CardCvvElement(cardCvcStyle)
#cardNumberElement.attach('#subscription-elements-number')
#cardMonthElement.attach('#subscription-elements-month')
#cardYearElement.attach('#subscription-elements-year')
#cardCvvElement.attach('#subscription-elements-cvv')
cardElement = @elements.CardElement(commonStyles)
cardElement.attach("#subscription-elements")
document.querySelector('#user-payment-submit').addEventListener('submit', @onSubmit.bind(this))
node.data('recurlied', true)
preparePaymentMethod: () ->
LocationActions.load()
setTimeout(() =>
@delayedConfigure()
, 200)
defaultText: () ->
'Select Country'
onStartPaypal: (e) ->
e.preventDefault()
if @state.updating
return
@setState({"paypal_updating": true})
@paypal.start()
onPayPalToken:(token) ->
console.log("OnPayPalToken", token)
@setState({updating: true})
rest.updatePayment({recurly_token: token.id}).done((response) => @updatePaymentDone(response)).fail((jqXHR) => @updatePaymentFailure(jqXHR))
onPayPalError: (err) ->
console.error("OnPayPalError", err)
@setState({"paypal_updating": false})
openBrowserToPayment: () ->
context.JK.popExternalLink("/client#/account/paymentHistory", true)
paymentMethod: () ->
if context.JK.isQWebEngine
return `<div>
<div classNames="column column-left">
<p>Updating payment is only supported in a web browser. Please click the button below to open this page in your system web browser.</p>
<p style={{textAlign: "center"}}>
<a className="button-orange" href='#' onClick={this.openBrowserToPayment}>UPDATE PAYMENT METHOD</a>
</p>
</div>
<br className="clearall"/>
</div>`
disabled = @state.updating || @reuseStoredCard()
submitClassNames = {'button-orange': true, 'purchase-btn': true, disabled: disabled && @state.updating}
updateCardClassNames = {'button-grey': true, 'update-btn': true, disabled: disabled && @state.updating}
backClassNames = {'button-grey': true, disabled: disabled && @state.updating}
cardNumberFieldClasses = {field: true, "card-number": true, error: @state.ccError}
expirationFieldClasses = {field: true, "expiration": true, error: @state.expiryError}
cvvFieldClasses = {field: true, "card-number": true, error: @state.cvvError}
inUSClasses = {field: true, "billing-in-us": true, error: @state.billingInUSError}
zipCodeClasses = {field: true, "zip-code": true, error: @state.zipCodeError}
nameClasses= {field: true, "name": true, error: @state.nameError}
firstNameClasses= {field: true, "first-name": true, error: @state.firstNameError}
lastNameClasses= {field: true, "last-name": true, error: @state.lastNameError}
address1Classes= {field: true, "address-1": true, error: @state.address1Error}
address2Classes= {field: true, "address-2": true, error: @state.address2Error}
cityClasses= {field: true, "city": true, error: @state.cityError}
stateClasses= {field: true, "state": true, error: @state.stateError}
countryClasses = {field: true, "country": true, error: @state.countryError}
formClasses= {stored: @reuseStoredCard()}
leftColumnClasses = {column: true, 'column-left': true, stored: @reuseStoredCard()}
rightColumnClasses = {column: true, 'column-right': true, stored: @reuseStoredCard()}
if @state.countries?
countries = [`<option key="" value="">{this.defaultText()}</option>`]
for countryId, countryInfo of @state.countries
countries.push(`<option key={countryId} value={countryId}>{countryInfo.name}</option>`)
country = @state.countries[this.currentCountry()]
else
countries = []
countryJsx = `
<select disabled={disabled} name="countries" onChange={this.onCountryChanged} value={this.currentCountry()} data-recurly="country" autoComplete="shipping country" className="country">{countries}</select>`
if gon.global.paypal_admin_only
if @state.user?.admin
paypal_visibility = 'block'
else
paypal_visibility = 'none'
else
paypal_visibility = 'block'
paypalButtonClasses = "paypal-button "
if !@state.configuredRecurly
paypalButtonText = `<span className="paypal-txt">Loading PayPal ...</span>`
paypalButtonClasses = paypalButtonClasses + "paypal-updating"
else if @state.paypal_updating && @state.updating
paypalButtonClasses = paypalButtonClasses + "paypal-updating"
paypalButtonText = `<span className="paypal-txt">Updating your account ...</span>`
else
paypalButtonText = `<img src="https://www.paypalobjects.com/webstatic/en_US/i/buttons/PP_logo_h_100x26.png" alt="PayPal" />`
if @state.uncollectables.length > 0
uncollectable = @state.uncollectables[0]
uncollectableMessage = `<div className="uncollectable-msg">A charge for your music lesson with {uncollectable.teacher.name} failed. Please update your credit card information immediately so that we can pay the instructor. If you have called your credit card provider and believe there should be no problem with your card, please email us at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a> so that we can figure out what's gone wrong. Thank you!</div>`
if @state.pastDue
uncollectableMessage = `<div className="uncollectable-msg">A charge for your subscription has failed. Please update your credit card information immediately so that you can restore your plan.</div>`
if @state.user?['has_stored_credit_card?'] && @state.uncollectables.length == 0
if @state.userWantsUpdateCC
header = 'Please update your billing address and payment information below.'
updateCardAction = `<a className={classNames(updateCardClassNames)} onClick={this.onLockPaymentInfo}>NEVERMIND</a>`
actions = `<div className="actions">
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a>
{updateCardAction}
<a className={classNames(submitClassNames)} onClick={this.onSubmitForm}>SUBMIT CARD INFORMATION</a>
</div>`
else
header = 'You have have a payment method on file already.'
updateCardAction = `<a className={classNames(updateCardClassNames)} onClick={this.onUnlockPaymentInfo}>I'D LIKE TO UPDATE MY PAYMENT INFO</a>`
managedSubscriptionAction = `<a href="/client#/account/subscription" className="button-orange">MANAGE MY SUBSCRIPTION</a>`
actions = `<div className="actions">
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a>
{updateCardAction}
{managedSubscriptionAction}
</div>`
else
header = `<div>Please enter your billing address and payment information below.<br/><br/><span className="no-storage-note">This information is sent to <a href="https://recurly.com/customers/" rel="external">Recurly</a>, which is compliant with the PCI-DSS security standard. JamKazam does not receive or store your credit card.</span></div>`
actions = `<div className="actions">
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a><a
className={classNames(submitClassNames)} onClick={this.onSubmitForm}>SUBMIT CARD INFORMATION</a>
</div>`
firstNameField =
`<div className={classNames(firstNameClasses)}>
<label>First Name:</label>
<input disabled={disabled} type="text" name="first_name" className="first_name" data-recurly="first_name" required autoComplete="cc-given-name"></input>
</div>`
lastNameField =
`<div className={classNames(lastNameClasses)}>
<label>Last Name:</label>
<input disabled={disabled} type="text" name="last_name" className="last_name" data-recurly="last_name" required autoComplete="cc-surname"></input>
</div>`
`<div>
<div className={classNames(leftColumnClasses)}>
{uncollectableMessage}
<div className="paymethod-header">{header}</div>
<form autoComplete="on" onSubmit={this.onSubmit} className={classNames(formClasses)} id="user-payment-submit">
{firstNameField}
{lastNameField}
<div className={classNames(address1Classes)}>
<label>Address 1:</label>
<input type="text" data-recurly="address1" disabled={disabled} required autoComplete="shipping address-line1" className="address-1"></input>
</div>
<div className={classNames(address2Classes)}>
<label>Address 2:</label>
<input type="text" data-recurly="address2" disabled={disabled} autoComplete="shipping address-line2" className="address-2"></input>
</div>
<div className={classNames(cityClasses)}>
<label for="city">City:</label>
<input type="text" data-recurly="city" className="city" disabled={disabled} required autoComplete="shipping address-level2"></input>
</div>
<div className={classNames(stateClasses)}>
<label for="state">State:</label>
<input type="text" data-recurly="state" className="state" disabled={disabled} required autoComplete="shipping address-level1"></input>
</div>
<div className={classNames(zipCodeClasses)}>
<label>Postal Code:</label>
<input type="text" name="zip" data-recurly="postal_code" disabled={disabled} required autoComplete="shipping postal-code"
className="zip"></input>
</div>
<div className={classNames(countryClasses)}>
<label for="country">Country:</label>
{countryJsx}
</div>
<div className={classNames(cardNumberFieldClasses)}>
<label>Card:</label>
<span className="recurly-holder" id="subscription-elements"></span>
</div>
<input type="hidden" name="recurly-token" data-recurly="token"></input>
<div className="paypal-region" style={{display: paypal_visibility}}>
<div className="or-use-paypal">Or Use PayPal:</div>
<a href="/paypal/checkout/start" className={paypalButtonClasses} onClick={this.onStartPaypal}>
{paypalButtonText}
</a>
</div>
</form>
{actions}
</div>
<br className="clearall"/>
</div>`
paymentsToJamKazam: () ->
rows = []
uncollectables = []
for uncollectable in @state.uncollectables
date = context.JK.formatDate(uncollectable.last_billed_at_date, true)
paymentMethod = 'Credit Card'
amt = uncollectable.expected_price_in_cents
displayAmount = ' $' + (amt/100).toFixed(2)
if uncollectable['is_card_declined?']
reason = 'card declined'
else if uncollectable['is_card_expired?']
reason = 'card expired'
else
reason = 'charge fail'
row =
`<tr>
<td>{date}</td>
<td className="capitalize">{paymentMethod}</td>
<td>{uncollectable.description}</td>
<td className="capitalize">{reason}</td>
<td>{displayAmount}</td>
</tr>`
uncollectables.push(row)
if uncollectables.length > 0
uncollectableTable = `
<div className="uncollectables">
<div className="uncollectable-msg">You have unpaid lessons, which are listed immediately below. <a onClick={this.selectionMade.bind(this, this.TILE_PAYMENT_METHOD)}>Click here</a> to update your credit card info.</div>
<div className="table-header">Unpaid Lessons</div>
<table className="payment-table unpaid">
<thead>
<tr>
<th>CHARGED AT</th>
<th>DESCRIPTION</th>
<th>REASON</th>
<th>AMOUNT</th>
</tr>
</thead>
<tbody>
{uncollectables}
</tbody>
</table>
<div className="table-header second">Payments</div>
</div>`
for paymentHistory in @getCurrentList()
paymentMethod = 'Credit Card'
if paymentHistory.sale?
sale = paymentHistory.sale
amt = sale.recurly_total_in_cents
status = 'paid'
displayAmount = ' $' + (amt/100).toFixed(2)
date = context.JK.formatDate(sale.created_at, true)
items = []
for line_item in sale.line_items
items.push(line_item.product_info?.name)
description = items.join(', ')
else
# this is a recurly webhook
#transaction = paymentHistory.transaction
#amt = transaction.amount_in_cents
#status = transaction.transaction_type
#displayAmount = '($' + (amt/100).toFixed(2) + ')'
#date = context.JK.formatDate(transaction.transaction_at, true)
#description = transaction.admin_description
invoice = paymentHistory
amt = invoice.total_in_cents
status = invoice.state
displayAmount = '($' + (amt/100).toFixed(2) + ')'
date = context.JK.formatDate(invoice.created_at, true)
description = invoice.description
amountClasses = {status: status}
row =
`<tr>
<td>{date}</td>
<td>{description}</td>
<td className="capitalize">{status}</td>
<td className={classNames(amountClasses)}>{displayAmount}</td>
</tr>`
rows.push(row)
`<div>
{uncollectableTable}
<table className="payment-table">
<thead>
<tr>
<th>DATE</th>
<th>DESCRIPTION</th>
<th>STATUS</th>
<th>AMOUNT</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<a className="btn-next-pager" href="/api/sales?page=1">Next</a>
<div className="end-of-payments-list end-of-list">No more payment history</div>
<div className="input-aligner">
<a className="back button-grey" onClick={this.onBack}>BACK</a>
</div>
<br className="clearall" />
</div>`
selectionMade: (selection, e) ->
e.preventDefault()
#@getUncollectables()
@setState({selected: selection})
activeTile: (selected = this.state.selected) ->
if selected?
selected
else
@tiles()[-1]
createTileLink: (i, tile) ->
if this.state.selected?
active = this.state.selected == tile
else
active = i == 0
tileClasses = {activeTile: active, 'profile-tile': true}
tileClasses[@myRole()] = true
tileClasses = classNames(tileClasses)
classes = classNames({last: i == @tiles().length - 1})
return `<div key={i} className={tileClasses}><a className={classes}
onClick={this.selectionMade.bind(this, tile)}>{tile}</a></div>`
tiles: () ->
#if @viewerStudent()
# tiles = @STUDENT_TILES
#else
# tiles = @TEACHER_TILES
#tiles
@STUDENT_TILES
myRole: () ->
if @viewerStudent()
'student'
else
'teacher'
viewerStudent: () ->
#!@viewerTeacher()
true
viewerTeacher: () ->
this.state.user?.is_a_teacher
onCustomBack: (customBack, e) ->
e.preventDefault()
context.location = customBack
render: () ->
mainContent = @mainContent()
profileSelections = []
for tile, i in @tiles()
profileSelections.push(@createTileLink(i, tile, profileSelections))
profileNavClasses = {"profile-nav": true}
profileNavClasses[@myRole()] = true
profileNavClasses = classNames(profileNavClasses)
profileNav = `<div className={classNames(profileNavClasses)}>
{profileSelections}
</div>`
`<div className="content-body-scroller">
<div className="profile-header profile-head">
<div className="account-header">payment<br/>management:</div>
{profileNav}
<div className="clearall"></div>
</div>
<div className="profile-body">
<div className="profile-wrapper">
<div className="main-content">
{mainContent}
<br />
</div>
</div>
</div>
</div>`
onUpdate: (e) ->
e.preventDefault()
if this.state.updating
return
name = @root.find('input[name="name"]').val()
if @isSchoolManaged()
scheduling_communication = 'school'
else
scheduling_communication = 'teacher'
correspondence_email = @root.find('input[name="correspondence_email"]').val()
@setState({updating: true})
rest.updateSchool({
id: this.state.school.id,
name: name,
scheduling_communication: scheduling_communication,
correspondence_email: correspondence_email
}).done((response) => @onUpdateDone(response)).fail((jqXHR) => @onUpdateFail(jqXHR))
onUpdateDone: (response) ->
@setState({school: response, userSchedulingComm: null, schoolName: null, updateErrors: null, updating: false})
@app.layout.notify({title: "update success", text: "Your school information has been successfully updated"})
onSubmitForm: (e) ->
form = document.querySelector('#user-payment-submit')
e.preventDefault()
@onSubmit(form)
onSubmit: (form) ->
@resetErrors()
#e.preventDefault()
console.log("onSubmit")
if !window.recurly?
@app.layout.notify({
title: 'Payment System Not Loaded',
text: "Please refresh this page and try to enter your info again. Sorry for the inconvenience!"
})
else
errored = false
# do a client-side sweep 1st
if !@root.find('input.first_name').val()
errored = true
@setState({firstNameError: true})
if !@root.find('input.last_name').val()
errored = true
@setState({lastNameError: true})
if !@root.find('input.address-1').val()
errored = true
@setState({address1Error: true})
if !@root.find('input.city').val()
errored = true
@setState({cityError: true})
if !@root.find('input.state').val()
errored = true
@setState({stateError: true})
if !@root.find('select.country').val()
errored = true
@setState({countryError: true})
if !@root.find('input.zip').val()
errored = true
@setState({zipCodeError: true})
if errored
return
#form = event.target
@setState({updating: true})
window.recurly.token(@elements, form, @onRecurlyToken)
onUpdateFail: (jqXHR) ->
handled = false
@setState({updating: false})
if jqXHR.status == 422
errors = JSON.parse(jqXHR.responseText)
handled = true
@setState({updateErrors: errors})
else
console.log("error path not taken", jqXHR)
onSubmitOld: (e) ->
@resetErrors()
e.preventDefault()
if !window.Stripe?
@app.layout.notify({
title: 'Payment System Not Loaded',
text: "Please refresh this page and try to enter your info again. Sorry for the inconvenience!"
})
else
ccNumber = @root.find('input.card-number').val()
expiration = @root.find('input.expiration').val()
cvv = @root.find('input.cvv').val()
inUS = @root.find('input.billing-address-in-us').is(':checked')
zip = @root.find('input.zip').val()
error = false
if @state.shouldShowName
name = @root.find('#set-user-on-card').val()
if name.indexOf('Anonymous') > -1
@setState({nameError: true})
error = true
if !$.payment.validateCardNumber(ccNumber)
@setState({ccError: true})
error = true
bits = expiration.split('/')
if bits.length == 2
month = bits[0].trim()
year = bits[1].trim()
month = new Number(month)
year = new Number(year)
if year < 2000
year += 2000
if !$.payment.validateCardExpiry(month, year)
@setState({expiryError: true})
error = true
else
@setState({expiryError: true})
error = true
cardType = $.payment.cardType(ccNumber)
if !$.payment.validateCardCVC(cvv, cardType)
@setState({cvvError: true})
error = true
if inUS && (!zip? || zip == '')
@setState({zipCodeError: true})
if error
return
data = {
number: ccNumber,
cvc: cvv,
exp_month: month,
exp_year: year,
}
@setState({updating: true})
window.Stripe.card.createToken(data, (status, response) => (@stripeResponseHandler(status, response)));
stripeResponseHandler: (status, response) ->
console.log("stripe response", JSON.stringify(response))
if response.error
@setState({updating: false})
if response.error.code == "invalid_number"
@setState({ccError: true, cvvError: null, expiryError: null})
else if response.error.code == "invalid_cvc"
@setState({ccError: null, cvvError: true, expiryError: null})
else if response.error.code == "invalid_expiry_year" || response.error.code == "invalid_expiry_month"
@setState({ccError: null, cvvError: null, expiryError: true})
#@setState({userWantsUpdateCC: false})
#window.UserActions.refresh()
@storeCC(response.id)
storeCC: (token) ->
if this.state.billingInUS
zip = @root.find('input.zip').val()
data = {
token: token,
zip: zip,
test_drive: false,
normal: false
}
if @state.shouldShowName
data.name = @root.find('#set-user-on-card').val()
@setState({updating: true})
rest.submitStripe(data).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR))
stripeSubmitted: (response) ->
@setState({updating: false})
logger.debug("stripe submitted: " + JSON.stringify(response))
@setState({userWantsUpdateCC: false})
#if @state.shouldShowName
window.UserActions.refresh()
if response.past_due
context.JK.Banner.showAlert('Credit Card Updated', 'Thank you. Your credit card info has been updated.<br/><br/>We will try to bill any unpaid lessons within the next hour, and an email will be sent at that time.')
else
@app.layout.notify({title: 'Credit Card Updated', text: 'Your credit card info has been updated.'})
stripeSubmitFailure: (jqXHR) ->
@setState({updating: false})
handled = false
if jqXHR.status == 422
errors = JSON.parse(jqXHR.responseText)
if errors.errors.name?
@setState({name: errors.errors.name[0]})
handled = true
else if errors.errors.user?
@app.layout.notify({title: "Can't Update Credit Card", text: "You " + errors.errors.user[0] + '.' })
handled = true
if !handled
@app.notifyServerError(jqXHR, 'Credit Card Not Stored')
onUnlockPaymentInfo: (e) ->
e.preventDefault()
@setState({userWantsUpdateCC: true})
onLockPaymentInfo: (e) ->
e.preventDefault()
@setState({userWantsUpdateCC: false})
reuseStoredCard: () ->
!@state.userWantsUpdateCC && @state.user?['has_stored_credit_card?'] && @state.uncollectables.length == 0
})