<commit_before>jraphical = require 'jraphical'
payment   = require 'koding-payment'

forceRefresh  = yes
forceInterval = 60 * 3

module.exports = class JPaymentSubscription extends jraphical.Module

  {secure, dash, daisy, signature} = require 'bongo'

  {partition} = require 'bongo/lib/util'

  JUser    = require '../user'
  JPayment = require './index'

  @share()

  @set
    indexes         :
      uuid          : 'unique'
    sharedMethods   :
      static        :
        fetchUserSubscriptions:
          (signature Function)
        fetchUserSubscriptionsWithPlan:
          (signature Function)
      instance      :
        cancel:
          (signature Function)
        resume:
          (signature Function)
        checkUsage: [
          (signature Object, Function)
          (signature Object, Number, Function)
        ]
        checkQuota  :
          (signature Object, Function)
        debit: [
          (signature Object, Function)
          (signature Object, Number, Function)
        ]
        credit:
          (signature Object, Function)
        transitionTo:
          (signature Object, Function)
    schema          :
      uuid          : String
      planCode      : String
      userCode      : String
      couponCode    : String
      quantity      :
        type        : Number
        default     : 1
      status        :
        type        : String
        enum        : ['Unknown status!'
                      [
                        'active'
                        'canceled'
                        'expired'
                        'future'
                        'in_trial'
                        'live'
                        'past_due'
                      ]]
      activatedAt   : Date
      expiresAt     : Date
      renewAt       : Date
      feeAmount     : Number
      lastUpdate    : Number
      quantities    :
        type        : Object
        default     : -> {}
      usage         :
        type        : Object # "usage" is designed to mirror "quantities" from JPaymentPlan
        default     : {}
      paymentMethodId: String
      tags          : (require './schema').tags
    relationships   :
      linkedSubscription:
        targetType  : this
        as          : 'linked subscription'

  @fetchUserSubscriptions = secure ({ connection:{ delegate }}, callback) ->
    delegate.fetchPaymentMethods (err, paymentMethods) =>
      return callback err  if err
      subscriptions = {}
      queue = paymentMethods.map ({ paymentMethodId }) => =>
        @fetchSubscriptions paymentMethodId, (err, subs) ->
          return queue.fin err  if err
          subscriptions[paymentMethodId] = subs  if subs.length
          queue.fin()
      dash queue, -> callback null, subscriptions

  @checkUserSubscription = secure ({connection:{delegate}}, planCode, callback)->
    throw Error 'reimplement this!'

  isOwnedBy: (account, callback) ->
    account.hasTarget this, 'service subscription', callback

  refund: (options, callback) ->
    payment.createRefund @paymentMethodId, options, callback

  getEndDate: -> new Date Math.max +(@renewAt ? 0), +(@expiresAt ? 0)

  calculateRefund: ->
    now     = Date.now()
    begin   = +@activatedAt
    end     = +@getEndDate()
    usage   = (now - begin) / (end - begin)
    ratio   = (100 - (usage * 100)) / 100

    return Math.ceil ratio * @feeAmount # cents

  updateStatus: (status, callback) ->
    @update $set: { status }, callback

  invokeMethod: (method, options, callback) ->
    [callback, options] = [options, callback]  unless callback
    options ?= { @uuid }
    payment[method] options, (err, sub) =>
      return callback err  if err

      @updateStatus sub.status, (err) ->
        return callback err  if err

        callback null

  cancel: (callback) ->
    @invokeMethod 'cancelSubscription', callback

  terminate: (oldPlan, callback, applyRefund = yes) ->
    @invokeMethod 'terminateSubscription', (err) =>
      return callback err  if err
      amount = @calculateRefund()
      description =
        """
        Refund for the remaining days of your current plan: #{ oldPlan.title }
        """

      if applyRefund
        @refund { amount, description }, (err) ->
          return callback err  if err

          callback null, amount
      else
        callback null, 0


  resume: (callback) ->
    @fetchLinkedSubscription (err, subscription) =>
      return callback err  if err

      if subscription
        subscription.cancel (err) =>
          return callback err  if err

          @removeLinkedSubscription subscription, (err) =>
            return callback err  if err

            @invokeMethod 'reactivateSubscription', callback
      else
        @invokeMethod 'reactivateSubscription', callback

  checkUsage: (product, multiplyFactor, callback) ->
    [callback, multiplyFactor] = [multiplyFactor, callback]  unless callback
    multiplyFactor ?= 1
    {quantities} = product

    unless quantities?
      quantities = {}
      quantities[product.planCode] = 1

    spend = quantities
    @checkQuota {@usage, @couponCode, spend, multiplyFactor}, callback

  vmCouponQuota =
    "1FREEVM"   : 1
    "2FREEVMS"  : 2
    "3FREEVMS"  : 3
    "4FREEVMS"  : 4

  checkQuota: (options, callback) ->
    {usage, spend, couponCode, multiplyFactor} = options
    multiplyFactor ?= 1

    usages = for own planCode, quantity of spend
      planSize    = @quantities[planCode] or @plan.quantities[planCode] or 0
      usageAmount = usage[planCode] ? 0
      spendAmount = (spend[planCode] ? 0) * multiplyFactor

      planSize += vmCouponQuota[couponCode]  if couponCode of vmCouponQuota
      total = planSize - usageAmount - spendAmount

      { planCode, total }

    [ok, over] = partition usages, ({ total }) -> total >= 0

    if over.length > 0
    then callback { message: 'quota exceeded', ok, over }
    else callback null

  createFulfillmentNonce: ({ planCode }, isDebit, callback) ->
    JFulfillmentNonce = require './nonce'
<commit_msg>payment: initialize spend option as an empty object if it is not defined<commit_after>jraphical = require 'jraphical'
payment   = require 'koding-payment'

forceRefresh  = yes
forceInterval = 60 * 3

module.exports = class JPaymentSubscription extends jraphical.Module

  {secure, dash, daisy, signature} = require 'bongo'

  {partition} = require 'bongo/lib/util'

  JUser    = require '../user'
  JPayment = require './index'

  @share()

  @set
    indexes         :
      uuid          : 'unique'
    sharedMethods   :
      static        :
        fetchUserSubscriptions:
          (signature Function)
        fetchUserSubscriptionsWithPlan:
          (signature Function)
      instance      :
        cancel:
          (signature Function)
        resume:
          (signature Function)
        checkUsage: [
          (signature Object, Function)
          (signature Object, Number, Function)
        ]
        checkQuota  :
          (signature Object, Function)
        debit: [
          (signature Object, Function)
          (signature Object, Number, Function)
        ]
        credit:
          (signature Object, Function)
        transitionTo:
          (signature Object, Function)
    schema          :
      uuid          : String
      planCode      : String
      userCode      : String
      couponCode    : String
      quantity      :
        type        : Number
        default     : 1
      status        :
        type        : String
        enum        : ['Unknown status!'
                      [
                        'active'
                        'canceled'
                        'expired'
                        'future'
                        'in_trial'
                        'live'
                        'past_due'
                      ]]
      activatedAt   : Date
      expiresAt     : Date
      renewAt       : Date
      feeAmount     : Number
      lastUpdate    : Number
      quantities    :
        type        : Object
        default     : -> {}
      usage         :
        type        : Object # "usage" is designed to mirror "quantities" from JPaymentPlan
        default     : {}
      paymentMethodId: String
      tags          : (require './schema').tags
    relationships   :
      linkedSubscription:
        targetType  : this
        as          : 'linked subscription'

  @fetchUserSubscriptions = secure ({ connection:{ delegate }}, callback) ->
    delegate.fetchPaymentMethods (err, paymentMethods) =>
      return callback err  if err
      subscriptions = {}
      queue = paymentMethods.map ({ paymentMethodId }) => =>
        @fetchSubscriptions paymentMethodId, (err, subs) ->
          return queue.fin err  if err
          subscriptions[paymentMethodId] = subs  if subs.length
          queue.fin()
      dash queue, -> callback null, subscriptions

  @checkUserSubscription = secure ({connection:{delegate}}, planCode, callback)->
    throw Error 'reimplement this!'

  isOwnedBy: (account, callback) ->
    account.hasTarget this, 'service subscription', callback

  refund: (options, callback) ->
    payment.createRefund @paymentMethodId, options, callback

  getEndDate: -> new Date Math.max +(@renewAt ? 0), +(@expiresAt ? 0)

  calculateRefund: ->
    now     = Date.now()
    begin   = +@activatedAt
    end     = +@getEndDate()
    usage   = (now - begin) / (end - begin)
    ratio   = (100 - (usage * 100)) / 100

    return Math.ceil ratio * @feeAmount # cents

  updateStatus: (status, callback) ->
    @update $set: { status }, callback

  invokeMethod: (method, options, callback) ->
    [callback, options] = [options, callback]  unless callback
    options ?= { @uuid }
    payment[method] options, (err, sub) =>
      return callback err  if err

      @updateStatus sub.status, (err) ->
        return callback err  if err

        callback null

  cancel: (callback) ->
    @invokeMethod 'cancelSubscription', callback

  terminate: (oldPlan, callback, applyRefund = yes) ->
    @invokeMethod 'terminateSubscription', (err) =>
      return callback err  if err
      amount = @calculateRefund()
      description =
        """
        Refund for the remaining days of your current plan: #{ oldPlan.title }
        """

      if applyRefund
        @refund { amount, description }, (err) ->
          return callback err  if err

          callback null, amount
      else
        callback null, 0


  resume: (callback) ->
    @fetchLinkedSubscription (err, subscription) =>
      return callback err  if err

      if subscription
        subscription.cancel (err) =>
          return callback err  if err

          @removeLinkedSubscription subscription, (err) =>
            return callback err  if err

            @invokeMethod 'reactivateSubscription', callback
      else
        @invokeMethod 'reactivateSubscription', callback

  checkUsage: (product, multiplyFactor, callback) ->
    [callback, multiplyFactor] = [multiplyFactor, callback]  unless callback
    multiplyFactor ?= 1
    {quantities} = product

    unless quantities?
      quantities = {}
      quantities[product.planCode] = 1

    spend = quantities
    @checkQuota {@usage, @couponCode, spend, multiplyFactor}, callback

  vmCouponQuota =
    "1FREEVM"   : 1
    "2FREEVMS"  : 2
    "3FREEVMS"  : 3
    "4FREEVMS"  : 4

  checkQuota: (options, callback) ->
    {usage, spend, couponCode, multiplyFactor} = options
    multiplyFactor ?= 1
    spend ?= {}

    usages = for own planCode, quantity of spend
      planSize    = @quantities[planCode] or @plan.quantities[planCode] or 0
      usageAmount = usage[planCode] ? 0
      spendAmount = (spend[planCode] ? 0) * multiplyFactor

      planSize += vmCouponQuota[couponCode]  if couponCode of vmCouponQuota
      total = planSize - usageAmount - spendAmount

      { planCode, total }

    [ok, over] = partition usages, ({ total }) -> total >= 0

    if over.length > 0
    then callback { message: 'quota exceeded', ok, over }
    else callback null

  createFulfillmentNonce: ({ planCode }, isDebit, callback) ->
    JFulfillmentNonce = require './nonce'

{'fraction_non_alphanumeric': 0.09319026000825423, 'fraction_numerical': 0.005282707387536112, 'mean_word_length': 1.9652471855115028, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5449951', 'n_tokens_mistral': 3344, 'n_tokens_neox': 3216, 'n_words': 933}