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

789 lines
25 KiB
CoffeeScript

context = window
rest = context.JK.Rest()
logger = context.JK.logger
AppStore = context.AppStore
UserStore = context.UserStore
profileUtils = context.JK.ProfileUtils
@AccountPaymentHistoryScreen = React.createClass({
mixins: [
ICheckMixin,
Reflux.listenTo(AppStore, "onAppInit"),
Reflux.listenTo(UserStore, "onUserChanged")
]
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
@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
@root = $(@getDOMNode())
@iCheckify()
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()
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})
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
logger.debug("dropping refresh because no tile match", @activeTile)
refreshSales: () ->
@refreshing = true
rest.getSalesHistory(@currentQuery)
.done(@salesHistoryDone)
.fail(@salesHistoryFail)
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'
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})
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: 'payments to jamkazam',
updating: false,
billingInUS: true,
userWantsUpdateCC: 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>`
paymentMethod: () ->
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}
formClasses= {stored: @reuseStoredCard()}
leftColumnClasses = {column: true, 'column-left': true, stored: @reuseStoredCard()}
rightColumnClasses = {column: true, 'column-right': true, stored: @reuseStoredCard()}
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.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.onSubmit}>SUBMIT CARD INFORMATION</a>
</div>`
else
header = 'You have already entered a credit card in JamKazam.'
updateCardAction = `<a className={classNames(updateCardClassNames)} onClick={this.onUnlockPaymentInfo}>I'D LIKE TO UPDATE MY PAYMENT INFO</a>`
actions = `<div className="actions">
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a>
{updateCardAction}
</div>`
else
header = 'Please enter your billing address and payment information below.'
actions = `<div className="actions">
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a><a
className={classNames(submitClassNames)} onClick={this.onSubmit}>SUBMIT CARD INFORMATION</a>
</div>`
if @state.shouldShowName && @state.user?.name?
username = @state.user?.name
nameField =
`<div className={classNames(nameClasses)}>
<label>Name:</label>
<input id="set-user-on-card" disabled={disabled} type="text" name="name" className="name" defaultValue={username}></input>
</div>`
`<div>
<div className={classNames(leftColumnClasses)}>
{uncollectableMessage}
<div className="paymethod-header">{header}</div>
<form autoComplete="on" onSubmit={this.onSubmit} className={classNames(formClasses)}>
{nameField}
<div className={classNames(cardNumberFieldClasses)}>
<label>Card Number:</label>
<input placeholder="1234 5678 9123 4567" type="tel" autoComplete="cc-number" disabled={disabled}
type="text" name="card-number" className="card-number"></input>
</div>
<div className={classNames(expirationFieldClasses)}>
<label>Expiration Date:</label>
<input placeholder="MM / YY" autoComplete="cc-expiry" disabled={disabled} type="text" name="expiration"
className="expiration"></input>
</div>
<div className={classNames(cvvFieldClasses)}>
<label>CVV:</label>
<input autoComplete="off" disabled={disabled} type="text" name="cvv" className="cvv"></input>
</div>
<div className={classNames(zipCodeClasses)}>
<label>Zip Code</label>
<input autoComplete="off" disabled={disabled || !this.state.billingInUS} type="text" name="zip"
className="zip"></input>
</div>
<div className={classNames(inUSClasses)}>
<label>Billing Address<br/>is in the U.S.</label>
<input type="checkbox" name="billing-address-in-us" className="billing-address-in-us"
value={this.state.billingInUS}/>
</div>
<input style={{'display':'none'}} type="submit" name="submit"/>
</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>METHOD</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
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>
{uncollectableTable}
<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">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
myRole: () ->
if @viewerStudent()
'student'
else
'teacher'
viewerStudent: () ->
!@viewerTeacher()
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/>history:</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"})
onUpdateFail: (jqXHR) ->
handled = false
@setState({updating: false})
if jqXHR.status == 422
errors = JSON.parse(jqXHR.responseText)
handled = true
@setState({updateErrors: errors})
onSubmit: (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.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: '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
})