oinak
6/15/2016 - 3:34 PM

Dummy backend for credit card form UI/UX exercise

Dummy backend for credit card form UI/UX exercise

source 'https://rubygems.org'

ruby '2.3.0'

gem 'sinatra'
# myapp.rb
require 'sinatra'
require 'json'
require_relative 'payment'

get '/' do
  erb :index
end

post '/' do
  request.body.rewind
  payment = Payment.create(request.body)
  [payment.status, payment.to_json]
end

__END__

@@ layout
<html>
<body>
  <%= yield %>

@@ index
<h1>Payments API:</h1>
<h2>GET '/'</h2>
<p> this page </p>
<h2>POST '/'</h2>
<h3>request:</h3>
<b>body:</b>
<tt> { "card": "1234567812345678","cvc": 123, "month": 1, "year": 2099, "cents": 500 } </tt>
<h3>response</h3>
<ul>
  <li>
    <b>success:</b>
    <p>status: <tt>200</tt> </p>
    <p>body: <tt>{ "id": "", "card": "1234567812345678", "cvc": 123, "month": 1, "year": 2099, "cents": 500, "errors": [] }</tt></p>
  </li>
  <li><b>failure:</b>
    <p>status: <tt>422</tt></p>
    <p>body:
      <tt>{"id":null,"card":"4242424242424242","cvc":"123","month":"1","year":"2016", "cents": 500,"errors":{"date":"card is expired"}} </tt>
    </p>
  </li>
</ul>
require 'json'

Payment = Struct.new(:card, :cvc, :month, :year, :cents) do
  class << self
    def create(datum)
      new(datum)
    end
  end

  attr_reader :data, :errors, :id

  def initialize(json = '')
    @data = JSON.load(json) || {}
    %w(card cents cvc month year).each { |k| send("#{k}=", @data[k]) }
    id
  end

  def id
    @id ||= valid? ? Time.now.strftime("%Y%m%d%k%M%S%L") : nil
  end

  def status
    valid? ? 200 : 422
  end

  def to_json
    JSON.dump(id:     id,
              card:   card,
              cvc:    cvc,
              month:  month,
              year:   year,
              cents:  cents,
              errors: errors)
  end

  private

  ERRORS = {
    data: 'wrong data format',
    cvc: 'must be a 3 digit number',
    card: 'card is invalid',
    date: 'card is expired',
    cents: 'must be a positive integer'
  }.freeze

  def valid?
    validate
    errors.empty?
  end

  def validate
    error?(:data) { |d| d.is_a?(Hash) }
    error?(:data) { |d| d.keys.sort == %w(card cents cvc month year) }
    error?(:card) { |d| d =~ /\d{16}/ }
    error?(:cvc) { |d| d =~ /\d{3}/ }
    error?(:date) { |d| future?(d[:month].to_i, d[:year].to_i) }
    error?(:cents) { |c| c.is_a?(Integer) && c > 0 }
  end

  def date
    { month: month, year: year }
  end

  def error?(key)
    value = send(key)
    errors.merge!(key =>ERRORS[key]) unless yield(value)
  end

  def errors
    @errors ||= {}
  end

  def future?(month, year)
    today = Date.today
    (year > today.year || year == today.year && month > today.month)
  end
end