kpm 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d38b77f40a5cdb50c139255664892b3f463a44bf
4
- data.tar.gz: 41019b6c0119626701a6a89823237585d7588bf6
3
+ metadata.gz: 4f1e196bb197de525d51d65751e802c910921afa
4
+ data.tar.gz: 64e8c82a289a508aa3bb76ff84671a5da4120ca9
5
5
  SHA512:
6
- metadata.gz: b0457be8ee32667355291e3b936ec9aeeaba943480a6006a5fe12f7cf7175d6445ca3663b8a4435b8cbabf0b9205f8ce8949f4dd5d535f8b64254e5cae04b4f4
7
- data.tar.gz: 0a14351c90df0618a2013a1a12b43001b4e483228a5af3e7f8fb098028b30914081e13077c9db4c5e497ca0bd4d6e7b3a83b04759fd2b140d23882c188ea93b1
6
+ metadata.gz: 8d95f6091a305f3c5c208a8e1aad22600762d3d7b5422ba0b94fa2193a4d309140ff03064a4c36ee94f1985833d7547710b334e0f50f05c77c14e6a1f7c16945
7
+ data.tar.gz: 149dc3e06978318594845dce2ae8f91025fe9a0b73f799d0655d18b74ff3af3c4b4ee782680b08a09bd31474df25fd88ee74d0310d823a8c1ea0d3cfdee4cf98
data/README.md CHANGED
@@ -147,6 +147,31 @@ ________________________________________________________________________________
147
147
  _______________________________________________________________________________________________________________________________________________________
148
148
 
149
149
  ```
150
+ ### Test required setups
151
+
152
+ There are 3 suites of tests for KPM (see `rake -T`):
153
+
154
+ * `rake test:spec` : Fast suite of unit tests
155
+ * `rake test:remote:spec` : Test suite that relies on maven artifacts
156
+ * `rake test:mysql:spec` : Test suite that requires an instance of Kill Bill server running and a properly setup database
157
+
158
+ #### KPM Unit test
159
+
160
+ Unit tests don't require any third party system or configuration.
161
+
162
+ #### KPM remote test
163
+
164
+ Test suite that verifies the following:\
165
+
166
+ * KPM `install` command by pulling artifacts from maven repository
167
+ * KPM `migration` command. This requires setting the `TOKEN` system property with a valid GITHUB api token.
168
+
169
+ #### KPM mysql test
170
+
171
+ Test suite that requires an instance of `mysql` running and verifies the following:
172
+
173
+ * KPM `account` command: The `account_spec.yml` file needs to be modified with correct credentials and user must have correct privileges; also the database schema must not exist.
174
+ In addition, one must start an instance of a Kill Bill server
150
175
 
151
176
  ## Internals
152
177
 
data/Rakefile CHANGED
@@ -20,6 +20,14 @@ namespace :test do
20
20
  task.pattern = './spec/*/remote/*_spec.rb'
21
21
  end
22
22
  end
23
+
24
+ namespace :mysql do
25
+ desc 'Run RSpec MySql related tests'
26
+ RSpec::Core::RakeTask.new do |task|
27
+ task.name = 'spec'
28
+ task.pattern = './spec/*/unit_mysql/*_spec.rb'
29
+ end
30
+ end
23
31
  end
24
32
 
25
33
  # Run tests by default
@@ -46,5 +46,6 @@ Gem::Specification.new do |s|
46
46
 
47
47
  s.add_development_dependency 'rake', '>= 10.0.0', '< 11.0.0'
48
48
  s.add_development_dependency 'rspec', '~> 2.12.0'
49
+ s.add_development_dependency 'killbill-client', '~> 1.0'
49
50
  end
50
51
 
data/lib/kpm.rb CHANGED
@@ -18,6 +18,8 @@ module KPM
18
18
  autoload :PluginsDirectory, 'kpm/plugins_directory'
19
19
  autoload :Migrations, 'kpm/migrations'
20
20
  autoload :System, 'kpm/system'
21
+ autoload :Account, 'kpm/account'
22
+ autoload :Database, 'kpm/database'
21
23
 
22
24
  class << self
23
25
  def root
@@ -0,0 +1,527 @@
1
+ require 'net/http'
2
+ require 'tmpdir'
3
+ require 'yaml'
4
+ require 'date'
5
+ require 'securerandom'
6
+
7
+ module KPM
8
+
9
+ class Account
10
+
11
+ # Killbill server
12
+ KILLBILL_HOST = ENV['KILLBILL_HOST'] || '127.0.0.1'
13
+ KILLBILL_URL = 'http://'.concat(KILLBILL_HOST).concat(':8080')
14
+ KILLBILL_API_VERSION = '1.0'
15
+
16
+ # USER/PWD
17
+ KILLBILL_USER = ENV['KILLBILL_USER'] || 'admin'
18
+ KILLBILL_PASSWORD = ENV['KILLBILL_PASSWORD'] || 'password'
19
+
20
+ # TENANT KEY
21
+ KILLBILL_API_KEY = ENV['KILLBILL_API_KEY'] || 'bob'
22
+ KILLBILL_API_SECRET = ENV['KILLBILL_API_SECRET'] || 'lazar'
23
+
24
+ # Temporary directory
25
+ TMP_DIR_PEFIX = 'killbill'
26
+ TMP_DIR = Dir.mktmpdir(TMP_DIR_PEFIX);
27
+
28
+ # Created By
29
+ WHO = 'kpm_export_import'
30
+
31
+ # safe payment method
32
+ SAFE_PAYMENT_METHOD = '__EXTERNAL_PAYMENT__'
33
+ PLUGIN_NAME_COLUMN = 'plugin_name'
34
+
35
+ # fields to remove from the export files
36
+ REMOVE_DATA_FROM = {:accounts => [:name, :address1, :address2, :city, :state_or_province, :phone, :email],
37
+ :account_history => [:name, :address1, :address2, :city, :state_or_province, :phone, :email]}
38
+
39
+ DATE_COLUMNS_TO_FIX = ['created_date','updated_date','processing_available_date','effective_date',
40
+ 'boot_date','start_timestamp','last_access_time','payment_date','original_created_date',
41
+ 'last_sys_update_date','charged_through_date','bundle_start_date','start_date']
42
+
43
+ # round trip constants duplicate record
44
+ ROUND_TRIP_EXPORT_IMPORT_MAP = {:accounts => {:id => :accounts_id, :external_key => :accounts_id}, :all => {:account_id => :accounts_id},
45
+ :account_history => {:id => :account_history_id, :external_key => :accounts_id, :payment_method_id => :payment_methods_id},
46
+ :account_emails => {:id => :account_emails_id}, :account_email_history => {:id => :account_email_history_id},
47
+ :subscription_events => {:id => :subscription_events_id},:subscriptions => {:id => :subscriptions_id},
48
+ :bundles => {:id => :bundles_id},:blocking_states => {:id => :blocking_states_id, :blockable_id => nil},
49
+ :invoice_items => {:id => :invoice_items_id, :child_account_id => nil, :invoice_id => :invoices_id, :bundle_id => :bundles_id, :subscription_id => :subscriptions_id },
50
+ :invoices => {:id => :invoices_id},
51
+ :invoice_payments => {:id => :invoice_payments_id, :invoice_id => :invoices_id, :payment_id => :payments_id},
52
+ :invoice_parent_children => {:id => :invoice_parent_children_id, :parent_invoice_id => nil, :child_invoice_id => nil, :child_account_id => nil},
53
+ :payment_attempts => {:id => :payment_attempts_id, :payment_method_id => :payment_methods_id, :transaction_id => :payment_transactions_id},
54
+ :payment_attempt_history => {:id => :payment_attempt_history_id, :payment_method_id => :payment_methods_id, :transaction_id => :payment_transactions_id},
55
+ :payment_methods => {:id => :payment_methods_id, :external_key => :generate},:payment_method_history => {:id => :payment_method_history_id},
56
+ :payments => {:id => :payments_id, :payment_method_id => :payment_methods_id},
57
+ :payment_history => {:id => :payment_history_id, :payment_method_id => :payment_methods_id},
58
+ :payment_transactions => {:id => :payment_transactions_id, :payment_id => :payments_id},
59
+ :payment_transaction_history => {:id => :payment_transaction_history_id, :payment_id => :payments_id},
60
+ :_invoice_payment_control_plugin_auto_pay_off => {:payment_method_id => :payment_methods_id, :payment_id => :payments_id},
61
+ :rolled_up_usage => {:id => :rolled_up_usage_id, :subscription_id => :subscriptions_id, :tracking_id => nil},
62
+ :custom_fields => {:id => :custom_fields_id},:custom_field_history => {:id => :custom_field_history_id},
63
+ :tag_definitions => {:id => :tag_definitions_id},:tag_definition_history => {:id => :tag_definition_history_id},
64
+ :tags => {:id => :tags_id, :object_id => nil},
65
+ :tag_history => {:id => :tag_history_id, :object_id => nil},
66
+ :audit_log => {:id => :audit_log_id}
67
+ }
68
+
69
+ #delimeters to sniff
70
+ DELIMITERS = [',','|']
71
+ DEFAULT_DELIMITER = "|"
72
+
73
+ def initialize(config_file = nil, killbill_api_credentials = nil, killbill_credentials = nil, killbill_url = nil,
74
+ database_name = nil, database_credentials = nil, data_delimiter = nil, logger = nil)
75
+ @killbill_api_key = KILLBILL_API_KEY
76
+ @killbill_api_secrets = KILLBILL_API_SECRET
77
+ @killbill_url = KILLBILL_URL
78
+ @killbill_user = KILLBILL_USER
79
+ @killbill_password = KILLBILL_PASSWORD
80
+ @delimiter = data_delimiter || DEFAULT_DELIMITER
81
+ @logger = logger
82
+ @tables_id = Hash.new
83
+
84
+
85
+ set_killbill_options(killbill_api_credentials,killbill_credentials,killbill_url)
86
+ set_database_options(database_name,database_credentials,logger)
87
+
88
+ load_config_from_file(config_file)
89
+
90
+ end
91
+
92
+ def export_data(account_id = nil)
93
+
94
+ if account_id === :export.to_s
95
+ raise Interrupt, 'Need to specify an account id'
96
+ end
97
+
98
+ export_data = fetch_export_data(account_id)
99
+ export_file = export(export_data)
100
+
101
+ if not File.exist?(export_file)
102
+ raise Interrupt, 'Account id not found'
103
+ else
104
+ @logger.info "\e[32mData exported under #{export_file}\e[0m"
105
+ end
106
+
107
+ export_file
108
+ end
109
+
110
+ def import_data(source_file,tenant_record_id, skip_payment_methods, round_trip_export_import = false, generate_record_id = false)
111
+
112
+ @generate_record_id = generate_record_id
113
+ @tenant_record_id = tenant_record_id
114
+ @round_trip_export_import = round_trip_export_import
115
+
116
+ if source_file === :import.to_s
117
+ raise Interrupt, 'Need to specify a file'
118
+ end
119
+
120
+ if not File.exist?(source_file)
121
+ raise Interrupt, 'Need to specify a valid file'
122
+ end
123
+
124
+ @delimiter = sniff_delimiter(source_file) || @delimiter
125
+
126
+ sanitize_and_import(source_file, skip_payment_methods)
127
+ end
128
+
129
+ private
130
+
131
+ # export helpers: fetch_export_data; export; process_export_data; remove_export_data;
132
+ def fetch_export_data(account_id)
133
+ uri = URI("#{@killbill_url}/#{KILLBILL_API_VERSION}/kb/export/#{account_id}")
134
+
135
+ request = Net::HTTP::Get.new(uri.request_uri)
136
+ request.basic_auth(@killbill_user,@killbill_password)
137
+ request['X-Killbill-ApiKey'] = @killbill_api_key;
138
+ request['X-Killbill-ApiSecret'] = @killbill_api_secrets;
139
+ request['X-Killbill-CreatedBy'] = WHO;
140
+
141
+ response = Net::HTTP.start(uri.host,uri.port) do |http|
142
+ http.request(request)
143
+ end
144
+
145
+ if response.to_s.include? 'HTTPUnauthorized'
146
+ raise Interrupt, "User is unauthorized -> \e[93mUser[#{@killbill_user}],password[#{@killbill_password}],api_key[#{@killbill_api_key}],api_secret[#{@killbill_api_secrets}]\e[0m"
147
+ end
148
+
149
+ if not response.is_a?(Net::HTTPSuccess)
150
+ raise Interrupt, 'Account id not found'
151
+ end
152
+
153
+ response.body
154
+ end
155
+
156
+ def export(export_data)
157
+ export_file = TMP_DIR + File::SEPARATOR + 'kbdump'
158
+
159
+ open (export_file), 'w' do |io|
160
+
161
+ table_name = nil
162
+ cols_names = nil
163
+ export_data.split("\n").each do |line|
164
+ words = line.strip.split(" ")
165
+ clean_line = line
166
+ if not /--/.match(words[0]).nil?
167
+ table_name = words[1]
168
+ cols_names = words[2].strip.split(@delimiter)
169
+ elsif not table_name.nil?
170
+ clean_line = process_export_data(line,table_name,cols_names)
171
+ end
172
+ io.puts clean_line
173
+
174
+ end
175
+
176
+ end
177
+
178
+ export_file
179
+ end
180
+
181
+ def process_export_data(line_to_process, table_name, cols_names)
182
+ clean_line = line_to_process
183
+
184
+ row = []
185
+ cols = clean_line.strip.split(@delimiter)
186
+ cols_names.each_with_index { |col_name, index|
187
+ sanitized_value = remove_export_data(table_name,col_name,cols[index])
188
+
189
+ row << sanitized_value
190
+
191
+ }
192
+
193
+ clean_line = row.join(@delimiter)
194
+
195
+ clean_line
196
+ end
197
+
198
+ def remove_export_data(table_name,col_name,value)
199
+
200
+ if not REMOVE_DATA_FROM[table_name.to_sym].nil?
201
+
202
+ if REMOVE_DATA_FROM[table_name.to_sym].include? col_name.to_sym
203
+ return nil
204
+ end
205
+
206
+ end
207
+
208
+ value
209
+ end
210
+
211
+ # import helpers: sanitize_and_import; import; sanitize; replace_tenant_record_id; replace_account_record_id; replace_boolean;
212
+ # fix_dates; fill_empty_column;
213
+ def sanitize_and_import(source_file, skip_payment_methods)
214
+ tables = Hash.new
215
+ error_importing_data = false
216
+
217
+ open (source_file), 'r' do |data|
218
+
219
+ rows = nil;
220
+ table_name = nil;
221
+ cols_names = nil;
222
+
223
+ data.each_line do |line|
224
+ words = line.strip.split(" ")
225
+
226
+ if /--/.match(words[0])
227
+ if not table_name.nil?
228
+ if @generate_record_id
229
+ cols_names.shift
230
+ end
231
+
232
+ tables[table_name] = { :col_names => cols_names, :rows => rows};
233
+ end
234
+
235
+ table_name = words[1]
236
+ cols_names = words[2].strip.split(@delimiter)
237
+
238
+ rows = []
239
+ elsif not table_name.nil?
240
+ row = process_import_data(line, table_name,cols_names, skip_payment_methods, rows)
241
+
242
+ next if row.nil?
243
+
244
+ rows.push(row)
245
+ else
246
+ error_importing_data = true
247
+ break
248
+ end
249
+ end
250
+
251
+ if not ( table_name.nil? || error_importing_data )
252
+ if @generate_record_id
253
+ cols_names.shift
254
+ end
255
+
256
+ tables[table_name] = { :col_names => cols_names, :rows => rows};
257
+ end
258
+
259
+ if tables.empty?
260
+ error_importing_data = true
261
+ end
262
+ end
263
+
264
+ if not error_importing_data
265
+ import(tables)
266
+ else
267
+ raise Interrupt, "Data on #{source_file} is invalid"
268
+ end
269
+
270
+ end
271
+
272
+ def process_import_data(line, table_name, cols_names, skip_payment_methods, rows)
273
+ cols = line.strip.split(@delimiter)
274
+
275
+ if cols_names.size != cols.size
276
+ return nil
277
+ end
278
+
279
+ row = []
280
+
281
+ cols_names.each_with_index do |col_name, index|
282
+ sanitized_value = sanitize(table_name,col_name,cols[index], skip_payment_methods)
283
+
284
+ if not sanitized_value.nil?
285
+ row << sanitized_value
286
+ end
287
+ end
288
+
289
+ return row
290
+ end
291
+
292
+ def import(tables)
293
+ record_id = nil;
294
+ statements = Database.generate_insert_statement(tables)
295
+ statements.each do |statement|
296
+ response = Database.execute_insert_statement(statement[:table_name],statement[:query], statement[:qty_to_insert], statement[:table_data],record_id)
297
+
298
+ if statement[:table_name] == 'accounts' && response.is_a?(String)
299
+ record_id = {:variable => '@account_record_id', :value => response}
300
+ end
301
+
302
+ if response === false
303
+ break
304
+ end
305
+ end
306
+
307
+ end
308
+
309
+ def sanitize(table_name,column_name,value,skip_payment_methods)
310
+ sanitized_value = replace_boolean(value)
311
+ sanitized_value = fill_empty_column(sanitized_value)
312
+
313
+ if table_name == 'payment_methods' && skip_payment_methods && column_name == PLUGIN_NAME_COLUMN
314
+ sanitized_value = SAFE_PAYMENT_METHOD
315
+ end
316
+
317
+ if DATE_COLUMNS_TO_FIX.include? column_name
318
+ sanitized_value = fix_dates(sanitized_value)
319
+ end
320
+
321
+ if not @tenant_record_id.nil?
322
+ sanitized_value = replace_tenant_record_id(table_name,column_name,sanitized_value)
323
+ end
324
+
325
+ if @generate_record_id
326
+ sanitized_value = replace_account_record_id(table_name,column_name,sanitized_value)
327
+ end
328
+
329
+ if @round_trip_export_import
330
+ sanitized_value = replace_uuid(table_name,column_name,sanitized_value)
331
+ end
332
+
333
+ sanitized_value
334
+ end
335
+
336
+ def replace_tenant_record_id(table_name,column_name,value)
337
+ return @tenant_record_id if column_name == 'tenant_record_id' || column_name == 'search_key2'
338
+ value
339
+ end
340
+
341
+ def replace_account_record_id(table_name,column_name,value)
342
+
343
+ if column_name == 'account_record_id'
344
+
345
+ return :@account_record_id
346
+ end
347
+
348
+ if column_name == 'record_id'
349
+ return nil
350
+ end
351
+
352
+ if column_name == 'target_record_id'
353
+
354
+ if table_name == 'account_history'
355
+ return :@account_record_id
356
+ end
357
+ end
358
+
359
+ if column_name == 'search_key1' && table_name == 'bus_ext_events_history'
360
+ return :@account_record_id
361
+ end
362
+
363
+ if column_name == 'search_key1' && table_name == 'bus_events_history'
364
+ return :@account_record_id
365
+ end
366
+
367
+ value
368
+
369
+ end
370
+
371
+ def replace_boolean(value)
372
+ if value.to_s === 'true'
373
+ return 1
374
+ elsif value.to_s === 'false'
375
+ return 0
376
+ else
377
+ return value
378
+ end
379
+ end
380
+
381
+ def fix_dates(value)
382
+ if !value.equal?(:DEFAULT)
383
+
384
+ dt = DateTime.parse(value)
385
+ return dt.strftime('%F %T').to_s
386
+
387
+ end
388
+
389
+ value
390
+ end
391
+
392
+ def fill_empty_column(value)
393
+ if value.to_s.strip.empty?
394
+ return :DEFAULT
395
+ else
396
+ return value
397
+ end
398
+ end
399
+
400
+ def replace_uuid(table_name,column_name,value)
401
+
402
+ if column_name == 'id'
403
+ @tables_id["#{table_name}_id"] = SecureRandom.uuid
404
+ end
405
+
406
+ if ROUND_TRIP_EXPORT_IMPORT_MAP[table_name.to_sym] && ROUND_TRIP_EXPORT_IMPORT_MAP[table_name.to_sym][column_name.to_sym]
407
+ key = ROUND_TRIP_EXPORT_IMPORT_MAP[table_name.to_sym][column_name.to_sym]
408
+
409
+ if key.equal?(:generate)
410
+ new_value = SecureRandom.uuid
411
+ else
412
+ new_value = @tables_id[key.to_s]
413
+ end
414
+
415
+ if new_value.nil?
416
+ new_value = SecureRandom.uuid
417
+ @tables_id[key.to_s] = new_value
418
+ end
419
+ return new_value
420
+ end
421
+
422
+ if not ROUND_TRIP_EXPORT_IMPORT_MAP[:all][column_name.to_sym].nil?
423
+ key = ROUND_TRIP_EXPORT_IMPORT_MAP[:all][column_name.to_sym]
424
+ new_value = @tables_id[key.to_s]
425
+
426
+ return new_value
427
+ end
428
+
429
+ value
430
+ end
431
+
432
+ def sniff_delimiter(file)
433
+
434
+ return nil if File.size?(file).nil?
435
+
436
+ first_line = File.open(file) {|f| f.readline}
437
+
438
+ return nil if first_line.nil?
439
+
440
+ sniff = {}
441
+
442
+ DELIMITERS.each do |delimiter|
443
+ sniff[delimiter] = first_line.count(delimiter)
444
+ end
445
+
446
+ sniff = sniff.sort {|a,b| b[1]<=>a[1]}
447
+ sniff.size > 0 ? sniff[0][0] : nil
448
+ end
449
+
450
+ # helper methods that set up killbill and database options: load_config_from_file; set_config; set_database_options;
451
+ # set_killbill_options;
452
+ def load_config_from_file(config_file)
453
+
454
+ set_config(config_file)
455
+
456
+ if not @config.nil?
457
+ config_killbill = @config['killbill']
458
+
459
+ if not config_killbill.nil?
460
+ set_killbill_options([config_killbill['api_key'],config_killbill['api_secret']],
461
+ [config_killbill['user'],config_killbill['password']],
462
+ "http://#{config_killbill['host']}:#{config_killbill['port']}")
463
+ end
464
+
465
+ config_db = @config['database']
466
+
467
+ if not config_db.nil?
468
+ set_database_options(config_db['database'],
469
+ [config_db['username'],config_db['password']],
470
+ @logger)
471
+
472
+ end
473
+ end
474
+ end
475
+
476
+ def set_config(config_file = nil)
477
+ @config = nil
478
+
479
+ if not config_file.nil?
480
+ if not Dir[config_file][0].nil?
481
+ @config = YAML::load_file(config_file)
482
+ end
483
+ end
484
+
485
+ end
486
+
487
+ def set_database_options(database_name = nil, database_credentials = nil, logger)
488
+
489
+ Database.set_logger(logger)
490
+
491
+ if not database_credentials.nil?
492
+ Database.set_credentials(database_credentials[0],database_credentials[1])
493
+ end
494
+
495
+ if not database_name.nil?
496
+ Database.set_database_name(database_name)
497
+ end
498
+
499
+ Database.set_mysql_command_line
500
+ end
501
+
502
+ def set_killbill_options(killbill_api_credentials, killbill_credentials, killbill_url)
503
+
504
+ if not killbill_api_credentials.nil?
505
+
506
+ @killbill_api_key = killbill_api_credentials[0]
507
+ @killbill_api_secrets = killbill_api_credentials[1]
508
+
509
+ end
510
+
511
+ if not killbill_credentials.nil?
512
+
513
+ @killbill_user = killbill_credentials[0]
514
+ @killbill_password = killbill_credentials[1]
515
+
516
+ end
517
+
518
+ if not killbill_url.nil?
519
+
520
+ @killbill_url = killbill_url
521
+
522
+ end
523
+ end
524
+
525
+ end
526
+
527
+ end