adyen 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/LICENSE +20 -0
- data/README.rdoc +32 -0
- data/Rakefile +5 -0
- data/adyen.gemspec +21 -0
- data/init.rb +1 -0
- data/lib/adyen.rb +31 -0
- data/lib/adyen/encoding.rb +21 -0
- data/lib/adyen/form.rb +71 -0
- data/lib/adyen/formatter.rb +37 -0
- data/lib/adyen/matchers.rb +105 -0
- data/lib/adyen/notification.rb +99 -0
- data/lib/adyen/soap.rb +147 -0
- data/spec/adyen_spec.rb +53 -0
- data/spec/form_spec.rb +123 -0
- data/spec/notification_spec.rb +97 -0
- data/spec/soap_spec.rb +30 -0
- data/spec/spec_helper.rb +11 -0
- data/tasks/github-gem.rake +323 -0
- metadata +103 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Willem van Bergen and Michel Barbosa
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
= Adyen
|
2
|
+
|
3
|
+
Package to simplify including the Adyen payments services into a Ruby on Rails application.
|
4
|
+
|
5
|
+
Adyen integration relies on three modes of communication between Adyen, your server and your client/customer:
|
6
|
+
|
7
|
+
* Client-to-Adyen communication using forms and redirects.
|
8
|
+
* Adyen-to-server communications using notifications.
|
9
|
+
* Server-to-Adyen communication using SOAP services.
|
10
|
+
|
11
|
+
This library aims to ease the implementation of all these modes into your application. Moreover, it provides matchers, assertions and mocks to make it easier to implement an automated test suite to assert the integration is working correctly.
|
12
|
+
|
13
|
+
== Installation
|
14
|
+
|
15
|
+
This plugin can either be installed as gem or Rails plugin:
|
16
|
+
|
17
|
+
gem install wvanbergen-adyen --source http://gems.github.com # as gem
|
18
|
+
script/plugin install git://github.com/wvanbergen/adyen.git # as plugin
|
19
|
+
|
20
|
+
== Usage
|
21
|
+
|
22
|
+
See the project wiki on http://wiki.github.com/wvanbergen/adyen to get started.
|
23
|
+
|
24
|
+
* For more information about Adyen, see http://www.adyen.com
|
25
|
+
* For more information about integrating Adyen, see their manuals at
|
26
|
+
http://support.adyen.com/links/documentation
|
27
|
+
|
28
|
+
== About
|
29
|
+
|
30
|
+
This package is written by Michel Barbosa and Willem van Bergen for Floorplanner.com,
|
31
|
+
and made public under the MIT license (see LICENSE). It comes without warranty of any kind,
|
32
|
+
so use at your own risk.
|
data/Rakefile
ADDED
data/adyen.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'adyen'
|
3
|
+
s.version = "0.1.4"
|
4
|
+
s.date = "2009-09-29"
|
5
|
+
|
6
|
+
s.summary = "Integrate Adyen payment services in you Ruby on Rails application"
|
7
|
+
s.description = "Package to simplify including the Adyen payments services into a Ruby on Rails application."
|
8
|
+
|
9
|
+
s.authors = ['Willem van Bergen', 'Michel Barbosa']
|
10
|
+
s.email = ['willem@vanbergen.org', 'cicaboo@gmail.com']
|
11
|
+
s.homepage = 'http://www.adyen.com'
|
12
|
+
|
13
|
+
s.add_development_dependency('rspec', '>= 1.1.4')
|
14
|
+
s.add_development_dependency('git', '>= 1.1.0')
|
15
|
+
|
16
|
+
s.rdoc_options << '--title' << s.name << '--main' << 'README.rdoc' << '--line-numbers' << '--inline-source'
|
17
|
+
s.extra_rdoc_files = ['README.rdoc']
|
18
|
+
|
19
|
+
s.files = %w(spec/spec_helper.rb lib/adyen/form.rb .gitignore LICENSE spec/soap_spec.rb spec/notification_spec.rb lib/adyen/soap.rb init.rb spec/adyen_spec.rb adyen.gemspec Rakefile tasks/github-gem.rake spec/form_spec.rb README.rdoc lib/adyen/notification.rb lib/adyen/matchers.rb lib/adyen/formatter.rb lib/adyen.rb lib/adyen/encoding.rb)
|
20
|
+
s.test_files = %w(spec/soap_spec.rb spec/notification_spec.rb spec/adyen_spec.rb spec/form_spec.rb)
|
21
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'adyen'
|
data/lib/adyen.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Adyen
|
2
|
+
LIVE_RAILS_ENVIRONMENTS = ['production']
|
3
|
+
|
4
|
+
# Setter voor the current Adyen environment.
|
5
|
+
# Must be either 'test' or 'live'
|
6
|
+
def self.environment=(env)
|
7
|
+
@environment = env
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns the current Adyen environment.
|
11
|
+
# Returns either 'test' or 'live'.
|
12
|
+
def self.environment(override = nil)
|
13
|
+
override || @environment || Adyen.autodetect_environment
|
14
|
+
end
|
15
|
+
|
16
|
+
# Autodetects the Adyen environment based on the RAILS_ENV constant
|
17
|
+
def self.autodetect_environment
|
18
|
+
(defined?(RAILS_ENV) && Adyen::LIVE_RAILS_ENVIRONMENTS.include?(RAILS_ENV.to_s.downcase)) ? 'live' : 'test'
|
19
|
+
end
|
20
|
+
|
21
|
+
# Loads submodules on demand, so that dependencies are not required.
|
22
|
+
def self.const_missing(sym)
|
23
|
+
require "adyen/#{sym.to_s.downcase}"
|
24
|
+
return Adyen.const_get(sym)
|
25
|
+
rescue
|
26
|
+
super(sym)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
require 'adyen/encoding'
|
31
|
+
require 'adyen/formatter'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'openssl'
|
3
|
+
require 'stringio'
|
4
|
+
require 'zlib'
|
5
|
+
|
6
|
+
module Adyen
|
7
|
+
module Encoding
|
8
|
+
def self.hmac_base64(hmac_key, message)
|
9
|
+
digest = OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), hmac_key, message)
|
10
|
+
Base64.encode64(digest).strip
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.gzip_base64(message)
|
14
|
+
sio = StringIO.new
|
15
|
+
gz = Zlib::GzipWriter.new(sio)
|
16
|
+
gz.write(message)
|
17
|
+
gz.close
|
18
|
+
Base64.encode64(sio.string).gsub("\n", "")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/adyen/form.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'action_view'
|
2
|
+
|
3
|
+
module Adyen
|
4
|
+
module Form
|
5
|
+
|
6
|
+
extend ActionView::Helpers::TagHelper
|
7
|
+
|
8
|
+
ACTION_URL = "https://%s.adyen.com/hpp/select.shtml"
|
9
|
+
|
10
|
+
def self.url(environment = nil)
|
11
|
+
environment ||= Adyen.environment(environment)
|
12
|
+
Adyen::Form::ACTION_URL % environment.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.calculate_signature_string(attributes)
|
16
|
+
merchant_sig_string = ""
|
17
|
+
merchant_sig_string << attributes[:payment_amount].to_s << attributes[:currency_code].to_s <<
|
18
|
+
attributes[:ship_before_date].to_s << attributes[:merchant_reference].to_s <<
|
19
|
+
attributes[:skin_code].to_s << attributes[:merchant_account].to_s <<
|
20
|
+
attributes[:session_validity].to_s << attributes[:shopper_email].to_s <<
|
21
|
+
attributes[:shopper_reference].to_s << attributes[:recurring_contract].to_s <<
|
22
|
+
attributes[:allowed_methods].to_s << attributes[:blocked_methods].to_s <<
|
23
|
+
attributes[:shopper_statement].to_s << attributes[:billing_address_type].to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.calculate_signature(attributes)
|
27
|
+
Adyen::Encoding.hmac_base64(attributes.delete(:shared_secret), calculate_signature_string(attributes))
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.do_attribute_transformations!(attributes = {})
|
31
|
+
raise "YENs are not yet supported!" if attributes[:currency_code] == 'JPY' # TODO: fixme
|
32
|
+
|
33
|
+
attributes[:recurring_contract] = 'DEFAULT' if attributes.delete(:recurring) == true
|
34
|
+
attributes[:order_data] = Adyen::Encoding.gzip_base64(attributes.delete(:order_data_raw)) if attributes[:order_data_raw]
|
35
|
+
attributes[:ship_before_date] = Adyen::Formatter::DateTime.fmt_date(attributes[:ship_before_date])
|
36
|
+
attributes[:session_validity] = Adyen::Formatter::DateTime.fmt_time(attributes[:session_validity])
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def self.hidden_fields(attributes = {})
|
41
|
+
do_attribute_transformations!(attributes)
|
42
|
+
|
43
|
+
raise "Cannot generate form: :currency code attribute not found!" unless attributes[:currency_code]
|
44
|
+
raise "Cannot generate form: :payment_amount code attribute not found!" unless attributes[:payment_amount]
|
45
|
+
raise "Cannot generate form: :merchant_account attribute not found!" unless attributes[:merchant_account]
|
46
|
+
raise "Cannot generate form: :skin_code attribute not found!" unless attributes[:skin_code]
|
47
|
+
raise "Cannot generate form: :shared_secret signing secret not provided!" unless attributes[:shared_secret]
|
48
|
+
|
49
|
+
# Merchant signature
|
50
|
+
attributes[:merchant_sig] = calculate_signature(attributes)
|
51
|
+
|
52
|
+
# Generate hidden input tags
|
53
|
+
attributes.map { |key, value|
|
54
|
+
self.tag(:input, :type => 'hidden', :name => key.to_s.camelize(:lower), :value => value)
|
55
|
+
}.join("\n")
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.redirect_signature_string(params)
|
59
|
+
params[:authResult].to_s + params[:pspReference].to_s + params[:merchantReference].to_s + params[:skinCode].to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.redirect_signature(params, shared_secret)
|
63
|
+
Adyen::Encoding.hmac_base64(shared_secret, redirect_signature_string(params))
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.redirect_signature_check(params, shared_secret)
|
67
|
+
params[:merchantSig] == redirect_signature(params, shared_secret)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Adyen
|
2
|
+
module Formatter
|
3
|
+
module DateTime
|
4
|
+
# Returns a valid Adyen string representation for a date
|
5
|
+
def self.fmt_date(date)
|
6
|
+
case date
|
7
|
+
when Date, DateTime, Time
|
8
|
+
date.strftime('%Y-%m-%d')
|
9
|
+
else
|
10
|
+
raise "Invalid date notation: #{date.inspect}!" unless /^\d{4}-\d{2}-\d{2}$/ =~ date
|
11
|
+
date
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns a valid Adyen string representation for a timestamp
|
16
|
+
def self.fmt_time(time)
|
17
|
+
case time
|
18
|
+
when Date, DateTime, Time
|
19
|
+
time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
20
|
+
else
|
21
|
+
raise "Invalid timestamp notation: #{time.inspect}!" unless /^\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z$/ =~ time
|
22
|
+
time
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Price
|
28
|
+
def self.in_cents(price)
|
29
|
+
((price * 100).round).to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.from_cents(price)
|
33
|
+
BigDecimal.new(price.to_s) / 100
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'xml'
|
2
|
+
|
3
|
+
module Adyen
|
4
|
+
module Matchers
|
5
|
+
|
6
|
+
module XPathPaymentFormCheck
|
7
|
+
|
8
|
+
def self.build_xpath_query(checks)
|
9
|
+
# Start by finding the check for the Adyen form tag
|
10
|
+
xpath_query = "//form[@action='#{Adyen::Form.url}']"
|
11
|
+
|
12
|
+
# Add recurring/single check if specified
|
13
|
+
recurring = checks.delete(:recurring)
|
14
|
+
unless recurring.nil?
|
15
|
+
if recurring
|
16
|
+
xpath_query << "[descendant::input[@type='hidden'][@name='recurringContract']]"
|
17
|
+
else
|
18
|
+
xpath_query << "[not(descendant::input[@type='hidden'][@name='recurringContract'])]"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Add a check for all the other fields specified
|
23
|
+
checks.each do |key, value|
|
24
|
+
condition = "descendant::input[@type='hidden'][@name='#{key.to_s.camelize(:lower)}']"
|
25
|
+
condition << "[@value='#{value}']" unless value == :anything
|
26
|
+
xpath_query << "[#{condition}]"
|
27
|
+
end
|
28
|
+
|
29
|
+
return xpath_query
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.document(subject)
|
33
|
+
if String === subject
|
34
|
+
XML::HTMLParser.string(subject).parse
|
35
|
+
elsif subject.respond_to?(:body)
|
36
|
+
XML::HTMLParser.string(subject.body).parse
|
37
|
+
elsif XML::Node === subject
|
38
|
+
subject
|
39
|
+
elsif XML::Document === subject
|
40
|
+
subject
|
41
|
+
else
|
42
|
+
raise "Cannot handle this XML input type"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.check(subject, checks = {})
|
47
|
+
document(subject).find_first(build_xpath_query(checks))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class HaveAdyenPaymentForm
|
52
|
+
|
53
|
+
def initialize(checks)
|
54
|
+
@checks = checks
|
55
|
+
end
|
56
|
+
|
57
|
+
def matches?(document)
|
58
|
+
Adyen::Matchers::XPathPaymentFormCheck.check(document, @checks)
|
59
|
+
end
|
60
|
+
|
61
|
+
def description
|
62
|
+
"have an adyen payment form"
|
63
|
+
end
|
64
|
+
|
65
|
+
def failure_message
|
66
|
+
"expected to find a valid Adyen form on this page"
|
67
|
+
end
|
68
|
+
|
69
|
+
def negative_failure_message
|
70
|
+
"expected not to find a valid Adyen form on this page"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def have_adyen_payment_form(checks = {})
|
75
|
+
default_checks = {:merchant_sig => :anything, :payment_amount => :anything, :currency_code => :anything, :skin_code => :anything }
|
76
|
+
HaveAdyenPaymentForm.new(default_checks.merge(checks))
|
77
|
+
end
|
78
|
+
|
79
|
+
def have_adyen_recurring_payment_form(checks = {})
|
80
|
+
recurring_checks = { :recurring => true, :shopper_email => :anything, :shopper_reference => :anything }
|
81
|
+
have_adyen_payment_form(recurring_checks.merge(checks))
|
82
|
+
end
|
83
|
+
|
84
|
+
def have_adyen_single_payment_form(checks = {})
|
85
|
+
recurring_checks = { :recurring => false }
|
86
|
+
have_adyen_payment_form(recurring_checks.merge(checks))
|
87
|
+
end
|
88
|
+
|
89
|
+
def assert_adyen_payment_form(subject, checks = {})
|
90
|
+
default_checks = {:merchant_sig => :anything, :payment_amount => :anything, :currency_code => :anything, :skin_code => :anything }
|
91
|
+
assert Adyen::Matchers::XPathPaymentFormCheck.check(subject, default_checks.merge(checks)), 'No Adyen payment form found'
|
92
|
+
end
|
93
|
+
|
94
|
+
def assert_adyen_recurring_payment_form(subject, checks = {})
|
95
|
+
recurring_checks = { :recurring => true, :shopper_email => :anything, :shopper_reference => :anything }
|
96
|
+
assert_adyen_payment_form(subject, recurring_checks.merge(checks))
|
97
|
+
end
|
98
|
+
|
99
|
+
def assert_adyen_single_payment_form(subject, checks = {})
|
100
|
+
recurring_checks = { :recurring => false }
|
101
|
+
assert_adyen_payment_form(subject, recurring_checks.merge(checks))
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'activerecord'
|
2
|
+
|
3
|
+
module Adyen
|
4
|
+
class Notification < ActiveRecord::Base
|
5
|
+
|
6
|
+
DEFAULT_TABLE_NAME = :adyen_notifications
|
7
|
+
set_table_name(DEFAULT_TABLE_NAME)
|
8
|
+
|
9
|
+
validates_presence_of :event_code
|
10
|
+
validates_presence_of :psp_reference
|
11
|
+
validates_uniqueness_of :success, :scope => [:psp_reference, :event_code]
|
12
|
+
|
13
|
+
# Make sure we don't end up with an original_reference with an empty string
|
14
|
+
before_validation { |notification| notification.original_reference = nil if notification.original_reference.blank? }
|
15
|
+
|
16
|
+
def self.log(params)
|
17
|
+
converted_params = {}
|
18
|
+
# Convert each attribute from CamelCase notation to under_score notation
|
19
|
+
# For example, merchantReference will be converted to merchant_reference
|
20
|
+
params.each do |key, value|
|
21
|
+
field_name = key.to_s.underscore
|
22
|
+
converted_params[field_name] = value if self.column_names.include?(field_name)
|
23
|
+
end
|
24
|
+
self.create!(converted_params)
|
25
|
+
end
|
26
|
+
|
27
|
+
def authorisation?
|
28
|
+
event_code == 'AUTHORISATION'
|
29
|
+
end
|
30
|
+
|
31
|
+
alias :authorization? :authorisation?
|
32
|
+
|
33
|
+
def successful_authorisation?
|
34
|
+
event_code == 'AUTHORISATION' && success?
|
35
|
+
end
|
36
|
+
|
37
|
+
def collect_payment_for_recurring_contract!(options)
|
38
|
+
# Make sure we convert the value to cents
|
39
|
+
options[:value] = Adyen::Formatter::Price.in_cents(options[:value])
|
40
|
+
raise "This is not a recurring contract!" unless event_code == 'RECURRING_CONTRACT'
|
41
|
+
Adyen::SOAP::RecurringService.submit(options.merge(:recurring_reference => self.psp_reference))
|
42
|
+
end
|
43
|
+
|
44
|
+
def deactivate_recurring_contract!(options)
|
45
|
+
raise "This is not a recurring contract!" unless event_code == 'RECURRING_CONTRACT'
|
46
|
+
Adyen::SOAP::RecurringService.deactivate(options.merge(:recurring_reference => self.psp_reference))
|
47
|
+
end
|
48
|
+
|
49
|
+
alias :successful_authorization? :successful_authorisation?
|
50
|
+
|
51
|
+
class HttpPost < Notification
|
52
|
+
|
53
|
+
def self.log(request)
|
54
|
+
super(request.params)
|
55
|
+
end
|
56
|
+
|
57
|
+
def live=(value)
|
58
|
+
self.write_attribute(:live, [true, 1, '1', 'true'].include?(value))
|
59
|
+
end
|
60
|
+
|
61
|
+
def success=(value)
|
62
|
+
self.write_attribute(:success, [true, 1, '1', 'true'].include?(value))
|
63
|
+
end
|
64
|
+
|
65
|
+
def value=(value)
|
66
|
+
self.write_attribute(:value, Adyen::Formatter::Price.from_cents(value)) unless value.blank?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Migration < ActiveRecord::Migration
|
71
|
+
|
72
|
+
def self.up(table_name = Adyen::Notification::DEFAULT_TABLE_NAME)
|
73
|
+
create_table(table_name) do |t|
|
74
|
+
t.boolean :live, :null => false, :default => false
|
75
|
+
t.string :event_code, :null => false
|
76
|
+
t.string :psp_reference, :null => false
|
77
|
+
t.string :original_reference, :null => true
|
78
|
+
t.string :merchant_reference, :null => false
|
79
|
+
t.string :merchant_account_code, :null => false
|
80
|
+
t.datetime :event_date, :null => false
|
81
|
+
t.boolean :success, :null => false, :default => false
|
82
|
+
t.string :payment_method, :null => true
|
83
|
+
t.string :operations, :null => true
|
84
|
+
t.text :reason
|
85
|
+
t.string :currency, :null => false, :limit => 3
|
86
|
+
t.decimal :value, :null => true, :precision => 9, :scale => 2
|
87
|
+
t.boolean :processed, :null => false, :default => false
|
88
|
+
t.timestamps
|
89
|
+
end
|
90
|
+
add_index table_name, [:psp_reference, :event_code, :success], :unique => true, :name => 'adyen_notification_uniqueness'
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.down(table_name = Adyen::Notification::DEFAULT_TABLE_NAME)
|
94
|
+
remove_index(table_name, :name => 'adyen_notification_uniqueness')
|
95
|
+
drop_table(table_name)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|