snf_core 0.2.94 → 0.2.97

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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/snf_core/auth_controller.rb +12 -44
  3. data/app/models/snf_core/account_transfer.rb +37 -0
  4. data/app/models/snf_core/item_request.rb +27 -0
  5. data/app/models/snf_core/quotation.rb +41 -0
  6. data/app/models/snf_core/user.rb +18 -18
  7. data/app/models/snf_core/virtual_account.rb +41 -0
  8. data/app/models/snf_core/virtual_account_transaction.rb +52 -0
  9. data/config/routes.rb +0 -3
  10. data/db/migrate/20250310122954_create_snf_core_virtual_accounts.rb +22 -0
  11. data/db/migrate/20250310123127_add_kyc_attributes_to_user.rb +17 -0
  12. data/db/migrate/20250310133249_create_snf_core_virtual_account_transactions.rb +18 -0
  13. data/db/migrate/20250310143604_create_snf_core_item_requests.rb +14 -0
  14. data/db/migrate/20250311000000_create_snf_core_account_transfers.rb +21 -0
  15. data/db/migrate/20250312043359_create_snf_core_quotations.rb +14 -0
  16. data/lib/snf_core/version.rb +1 -1
  17. data/spec/dummy/db/schema.rb +130 -27
  18. data/spec/dummy/log/development.log +2618 -0
  19. data/spec/dummy/log/test.log +110127 -0
  20. data/spec/dummy/tmp/storage/00/ch/00ch4m884d5bnelqgd2ld1qig6ey +0 -0
  21. data/spec/dummy/tmp/storage/0s/ba/0sbase6mg5sgich6or97cg1faxlw +0 -0
  22. data/spec/dummy/tmp/storage/0x/uy/0xuyfqa0t08zr8vfm8p123o0s34t +0 -0
  23. data/spec/dummy/tmp/storage/1o/ts/1otsbrqkd6n4qiht1wabl7fhjnt3 +0 -0
  24. data/spec/dummy/tmp/storage/1v/54/1v542i134jpb9iwbq9m1v801y0ls +0 -0
  25. data/spec/dummy/tmp/storage/1y/46/1y4617uv2nrk9fa58zfv9xyixmfk +0 -0
  26. data/spec/dummy/tmp/storage/24/kq/24kq40q48k7jry3npyah0td4uend +0 -0
  27. data/spec/dummy/tmp/storage/28/ye/28yeusf04wxopqkg0bxki7mqo9ph +0 -0
  28. data/spec/dummy/tmp/storage/2r/hv/2rhvs6uca2ozrojfuub5d4qk0ugy +0 -0
  29. data/spec/dummy/tmp/storage/2w/r1/2wr14buxnrx4cee19gj6sefnbby5 +0 -0
  30. data/spec/dummy/tmp/storage/3k/7z/3k7zlcd9fm2jwn99kinxpahrmm4u +0 -0
  31. data/spec/dummy/tmp/storage/3o/2a/3o2ahddfby8aqgutow91vo4asm4f +0 -0
  32. data/spec/dummy/tmp/storage/46/zj/46zjkljy84qmg4ebes40e2zolzp7 +0 -0
  33. data/spec/dummy/tmp/storage/4f/fl/4fflxsg2pissq4960wfazyv9mgp1 +0 -0
  34. data/spec/dummy/tmp/storage/4l/6a/4l6ap3t705ektu02kabqobaq8huf +0 -0
  35. data/spec/dummy/tmp/storage/4m/3n/4m3n3r5zl2jevvvw3alq0omnnowk +0 -0
  36. data/spec/dummy/tmp/storage/4t/qc/4tqc587gmu2g7hgntrgjnhkw0e1u +0 -0
  37. data/spec/dummy/tmp/storage/4w/76/4w7656628v1p4mp87fqz3okc6ix4 +0 -0
  38. data/spec/dummy/tmp/storage/50/ct/50ctjwt8y93spjaef1b3rfnav9gb +0 -0
  39. data/spec/dummy/tmp/storage/5m/0k/5m0k5zx08ouqos7rs09bpgmalspu +0 -0
  40. data/spec/dummy/tmp/storage/5m/4n/5m4nvbe3vqwrdpy6hujr1oq5nolv +0 -0
  41. data/spec/dummy/tmp/storage/5m/j8/5mj8ypl6zxx8g3h7vqmu4p2la4pk +0 -0
  42. data/spec/dummy/tmp/storage/5r/tv/5rtv2agppt9e52eg0oc81svs896e +0 -0
  43. data/spec/dummy/tmp/storage/5s/b6/5sb6kux9jtdj4du9ihu32tc82s12 +0 -0
  44. data/spec/dummy/tmp/storage/5t/84/5t84ncnwqnv83rndaztqz1dsae3v +0 -0
  45. data/spec/dummy/tmp/storage/6h/9a/6h9a6bxecnlwx7zbb4dc33rmftee +0 -0
  46. data/spec/dummy/tmp/storage/6h/pd/6hpdw0r7w6vpcotirwqz1fv640kk +0 -0
  47. data/spec/dummy/tmp/storage/6i/dm/6idmnfdfriqdmvcvgreyeho4xgra +0 -0
  48. data/spec/dummy/tmp/storage/6n/3b/6n3bufdfx1tvls9lzm3g625l1lyk +0 -0
  49. data/spec/dummy/tmp/storage/78/5u/785ub96ra353qygeubpq4vlifjas +0 -0
  50. data/spec/dummy/tmp/storage/7v/55/7v55c4y5xdiw6qrvbdpmd63v4toz +0 -0
  51. data/spec/dummy/tmp/storage/83/yi/83yiflnj2evgqahmzgiavbywle7h +0 -0
  52. data/spec/dummy/tmp/storage/8a/02/8a02q8tcm3lowda8cvcjh18tka7i +0 -0
  53. data/spec/dummy/tmp/storage/8g/4a/8g4aarquz0mos9gxzq6j7xzwli09 +0 -0
  54. data/spec/dummy/tmp/storage/8k/0m/8k0mv7xql5lr89d0172izqplypwx +0 -0
  55. data/spec/dummy/tmp/storage/8n/lz/8nlzdb800dgr9iax1ez2ko4euh02 +0 -0
  56. data/spec/dummy/tmp/storage/8y/y2/8yy2kqz8sa11vh7naiomyofj30ox +0 -0
  57. data/spec/dummy/tmp/storage/92/hz/92hz15r26fs5wymc6tced4b08bh6 +0 -0
  58. data/spec/dummy/tmp/storage/9k/wj/9kwjdz4yw0jyzddj32u97xciot47 +0 -0
  59. data/spec/dummy/tmp/storage/a0/ve/a0veqfafcpy1hl6jvg6jc4qt08kr +0 -0
  60. data/spec/dummy/tmp/storage/ak/g9/akg9bg7fdwisycbclvmikbzl676j +0 -0
  61. data/spec/dummy/tmp/storage/ap/fs/apfsy6cp4mt4jsyiyscutqhv36id +0 -0
  62. data/spec/dummy/tmp/storage/b2/ux/b2uxv1pu0hjqty7779grby8qxzoo +0 -0
  63. data/spec/dummy/tmp/storage/bh/s1/bhs1yc1jo5vhc1u8lz7zme0sowln +0 -0
  64. data/spec/dummy/tmp/storage/c6/29/c6299u91u1tc5bc7eb24yq8k7xgk +0 -0
  65. data/spec/dummy/tmp/storage/cb/dh/cbdhbsxm7irfbsrniqvxnfukotka +0 -0
  66. data/spec/dummy/tmp/storage/dj/zr/djzriitbyq5sebmjs7nf0wqijzl2 +0 -0
  67. data/spec/dummy/tmp/storage/dk/z6/dkz6yvma826vmeofy924nzacyi6a +0 -0
  68. data/spec/dummy/tmp/storage/dw/75/dw75t7gej4irzb3hcqssl8w5vhde +0 -0
  69. data/spec/dummy/tmp/storage/dz/dq/dzdq3gkuy7tdiue1yqmciu86nzfg +0 -0
  70. data/spec/dummy/tmp/storage/e6/5h/e65haeyr0sglopfuo5chz55thi9z +0 -0
  71. data/spec/dummy/tmp/storage/ez/kd/ezkdx2jgksxsm9fdwpx5qw8qd6hb +0 -0
  72. data/spec/dummy/tmp/storage/ft/pz/ftpzprhag9awyjvcac1xiqv9qngf +0 -0
  73. data/spec/dummy/tmp/storage/g1/fp/g1fprpwzsoltdd9atheqqbmt75w1 +0 -0
  74. data/spec/dummy/tmp/storage/g6/7r/g67rz6k8b5kmrfsk91e77d37vnfg +0 -0
  75. data/spec/dummy/tmp/storage/ik/r8/ikr86smhkvxy0yrqxjr0iubcuy5s +0 -0
  76. data/spec/dummy/tmp/storage/ip/yy/ipyyodp4f221bzp384ncz3alga7l +0 -0
  77. data/spec/dummy/tmp/storage/j4/ln/j4lnbjjufbp59yxsdetbqs0cyjia +0 -0
  78. data/spec/dummy/tmp/storage/jc/jk/jcjkdsojvg0n7abwcfpscbkrlvoj +0 -0
  79. data/spec/dummy/tmp/storage/jd/ik/jdik45lk030wwyyfs9xy6m4q0sv9 +0 -0
  80. data/spec/dummy/tmp/storage/jo/34/jo34sgi1pzpkbryh3z6bmeccbada +0 -0
  81. data/spec/dummy/tmp/storage/jw/gm/jwgmfnb1z5jahut7upw5lwnh6orj +0 -0
  82. data/spec/dummy/tmp/storage/kv/7m/kv7mdo1kvqnagfcp1dit4pzrlugm +0 -0
  83. data/spec/dummy/tmp/storage/kz/ao/kzao8av3jxls3bjeal7bv2qefcq2 +0 -0
  84. data/spec/dummy/tmp/storage/l8/1u/l81unawd0p9c61x8k6ofq0y227yt +0 -0
  85. data/spec/dummy/tmp/storage/lp/iz/lpizuy9o61mj9a6nsp66uhxmwifn +0 -0
  86. data/spec/dummy/tmp/storage/lu/ge/lugetp9a4lxzimpwttg2bkmrv1tf +0 -0
  87. data/spec/dummy/tmp/storage/m0/hl/m0hl9jcc1ra6ogfep0yxwon1n8e4 +0 -0
  88. data/spec/dummy/tmp/storage/md/94/md943ike1q6c3z19js6h89ghdqpd +0 -0
  89. data/spec/dummy/tmp/storage/mi/y2/miy2op7reeft1521or0hx3sa03qi +0 -0
  90. data/spec/dummy/tmp/storage/n4/6y/n46y5dyl1uf9ihlyp79fzdxny80f +0 -0
  91. data/spec/dummy/tmp/storage/ng/sy/ngsy8siky5us2x8cr54uxlzwoji5 +0 -0
  92. data/spec/dummy/tmp/storage/nq/9r/nq9rfa8upvcoyx92za4fhzrmob0t +0 -0
  93. data/spec/dummy/tmp/storage/nz/h5/nzh5f9vo9icpp0qvxvvm57ivsy85 +0 -0
  94. data/spec/dummy/tmp/storage/o3/3e/o33eo1zb3spyqgqbtugs43olucmj +0 -0
  95. data/spec/dummy/tmp/storage/oc/zy/oczyqb5chp2fvo9kdu62c3y7pl5i +0 -0
  96. data/spec/dummy/tmp/storage/of/zt/ofzt1knekv9ft8direbw6fiaacvc +0 -0
  97. data/spec/dummy/tmp/storage/oh/ls/ohlsxs36tio52r2ydrbzez1u0w26 +0 -0
  98. data/spec/dummy/tmp/storage/ol/u8/olu875ti78gguc82spdkt9z8tpii +0 -0
  99. data/spec/dummy/tmp/storage/pe/xm/pexmtjvhiiepbza9928j2j0uo6ub +0 -0
  100. data/spec/dummy/tmp/storage/q4/wp/q4wpnk0ajsmg3nnpj2xja15tc1az +0 -0
  101. data/spec/dummy/tmp/storage/q7/48/q748a4a7d3qk28vajhbkli5tttsj +0 -0
  102. data/spec/dummy/tmp/storage/qg/qu/qgquu27ysijim1jv06k39a3c3opr +0 -0
  103. data/spec/dummy/tmp/storage/qo/zx/qozxdnemk1fwxcvhe1j10ys7mxcq +0 -0
  104. data/spec/dummy/tmp/storage/qq/vt/qqvtf3h8ge74ce67gghth8ivch0g +0 -0
  105. data/spec/dummy/tmp/storage/rg/hw/rghwbui1nfqvuuja4hqvzvwofucm +0 -0
  106. data/spec/dummy/tmp/storage/rj/4a/rj4a0t2xebuf22tyhuitu7x0h0ct +0 -0
  107. data/spec/dummy/tmp/storage/ro/gg/roggtjpn82v1pf5hi9c72ta6uxt7 +0 -0
  108. data/spec/dummy/tmp/storage/sd/b5/sdb5dbqjlz1nng8qaejdmh94h5kh +0 -0
  109. data/spec/dummy/tmp/storage/t6/h0/t6h0whv9denben7ktf5maaohv9dg +0 -0
  110. data/spec/dummy/tmp/storage/u8/dc/u8dc1at72gpsq6d2fqb81rku7xq9 +0 -0
  111. data/spec/dummy/tmp/storage/v5/c9/v5c9opt3yikmy2gwg4ftmde0vyt5 +0 -0
  112. data/spec/dummy/tmp/storage/v8/cs/v8csqmcgvc4z177g5hfmp7twf6ay +0 -0
  113. data/spec/dummy/tmp/storage/vb/z1/vbz124po6xcx6fbh4nefle8r6of3 +0 -0
  114. data/spec/dummy/tmp/storage/vc/0v/vc0vb76xhipq8d5xrmqnnvlkbxhi +0 -0
  115. data/spec/dummy/tmp/storage/vc/eu/vceu8un4vczdune7r9oaihtx3l09 +0 -0
  116. data/spec/dummy/tmp/storage/vg/qn/vgqnfl62z1s3940ei2s9ifrjufiz +0 -0
  117. data/spec/dummy/tmp/storage/vz/qz/vzqz49sc3hfzbcaawqw6yuqzpocv +0 -0
  118. data/spec/dummy/tmp/storage/w1/9e/w19epnmenhmu0y14exloj2oskf53 +0 -0
  119. data/spec/dummy/tmp/storage/w4/k8/w4k8g0iv6qtdhzyw454un1d048hp +0 -0
  120. data/spec/dummy/tmp/storage/wv/8i/wv8iai6dxxh2wqwnj667p5rre1tn +0 -0
  121. data/spec/dummy/tmp/storage/xd/fp/xdfpr9gw2neskzyq6cfxyj2lsa47 +0 -0
  122. data/spec/dummy/tmp/storage/xh/4e/xh4e0tnk5ysrfndrhyu9u0ywueku +0 -0
  123. data/spec/dummy/tmp/storage/xj/d2/xjd2s558zk0eduk4f25ul4t29b2r +0 -0
  124. data/spec/dummy/tmp/storage/yi/r2/yir26w0pcj2268kp8biw0lcrnimz +0 -0
  125. data/spec/dummy/tmp/storage/yj/d7/yjd7k0ea2d8y6yjp8syfudd0qvhr +0 -0
  126. data/spec/dummy/tmp/storage/z0/il/z0ilfyp3hmqqn6alkocrt7y949ee +0 -0
  127. data/spec/dummy/tmp/storage/za/ov/zaov1yasuhakxa5olzoed94khnf8 +0 -0
  128. data/spec/dummy/tmp/storage/zc/d6/zcd6d4lsb12obz7m7r6240qka8xw +0 -0
  129. data/spec/dummy/tmp/storage/zc/i3/zci3kt86sywh9q6u7xv7yt38xznu +0 -0
  130. data/spec/examples.txt +199 -128
  131. data/spec/factories/snf_core/account_transfers.rb +27 -0
  132. data/spec/factories/snf_core/addresses.rb +1 -0
  133. data/spec/factories/snf_core/item_requests.rb +26 -0
  134. data/spec/factories/snf_core/quotations.rb +10 -0
  135. data/spec/factories/snf_core/users.rb +10 -1
  136. data/spec/factories/snf_core/virtual_account_transactions.rb +33 -0
  137. data/spec/factories/snf_core/virtual_accounts.rb +32 -0
  138. data/spec/models/snf_core/account_transfer_spec.rb +29 -0
  139. data/spec/models/snf_core/item_request_spec.rb +44 -0
  140. data/spec/models/snf_core/quotation_spec.rb +54 -0
  141. data/spec/models/snf_core/user_spec.rb +18 -22
  142. data/spec/models/snf_core/virtual_account_spec.rb +85 -0
  143. data/spec/models/snf_core/virtual_account_transaction_spec.rb +72 -0
  144. data/spec/requests/snf_core/auth_spec.rb +11 -85
  145. metadata +133 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85e0a48d15038fbf0986e432723ef4b5d6789e2795ba1702e81a0b933542df23
4
- data.tar.gz: c96837434bf4792ca8d109094aa2d21057dbba027e14f7ca781a215254a1ef63
3
+ metadata.gz: cb9e5ede5f83298df0848d30196251d3d490f3fa1392079f9d8fe0e454bd1547
4
+ data.tar.gz: 7e5e495288a370b7decf97033ce8cf29400a943257daa515ff7a1ceab536fdb5
5
5
  SHA512:
6
- metadata.gz: 22db5c47abc86de6990ba45f4785bd72c1ca7b456258da5e1c2fe503147eaec4df0d98025610c1eaa20c12265b412552e696603cc4d0a3bd2a02fc24039265c1
7
- data.tar.gz: 9275800e26a3904270dd786c221942238689ea060919d5fadfa1e788b6889ae308f5a9eb6e2270287340ad8e105d9e8497dcb1d6c6c37774411c457a4a3ba745
6
+ metadata.gz: 96c4594e509069c29e497000cce7d9275df7b182a12bc09dd5765059473460b3a85a934aca33956de45e5ffa8080b17130b0b4cbfcd082a59873bbb8ca9f995f
7
+ data.tar.gz: c77b67a9da71c5e6a5953d5612d87bcfa4a8de5f57b12d5cfc519e82ac520c1910231733dc5f30f823850484f9f5bcfdd57dcd83cffd489edad190bb9e843704
@@ -40,48 +40,6 @@ module SnfCore
40
40
  render json: { success: true, token: token }
41
41
  end
42
42
 
43
- def reset_password_request
44
- user = User.find_by(phone_number: auth_params[:phone_number])
45
- return render json: { success: false, error: "User doesn't exist" }, status: :unauthorized unless user
46
- user.reset_password_token = SecureRandom.hex
47
- user.save!
48
- AuthMailer.with(
49
- user: user,
50
- reset_url: params[:reset_url] || "http://localhost:3000/reset-password"
51
- ).reset_password.deliver_now
52
- render json: { success: true, message: "Reset password token sent to your email" }
53
- end
54
-
55
- def reset_password
56
- user = User.find_by(reset_password_token: auth_params[:reset_password_token])
57
- return render json: { success: false, error: "Token has expired please try again!" }, status: :unauthorized unless user
58
- if user.reset_password_token == auth_params[:reset_password_token]
59
- return render json: { success: false, error: "You can't use the same password as your current password" }, status: :unprocessable_entity if BCrypt::Password.new(user.password_digest) == auth_params[:password]
60
- user.password = auth_params[:password]
61
- user.reset_password_token = nil
62
- user.password_changed = true
63
- user.save!
64
- render json: { success: true, message: "Password reset successfully" }
65
- else
66
- user.reset_password_token = nil
67
- user.save!
68
- render json: { success: false, error: "Reset password token doesn't match try again" }, status: :unprocessable_entity
69
- end
70
- end
71
-
72
- def change_password
73
- user = current_user
74
- if user && user.authenticate(auth_params[:password])
75
- return render json: { success: false, error: "Can't use the same password as your current password" }, status: :unprocessable_entity if BCrypt::Password.new(user.password_digest) == auth_params[:new_password]
76
- user.password = auth_params[:new_password]
77
- user.password_changed = true
78
- user.save!
79
- render json: { success: true, message: "Password changed successfully" }
80
- else
81
- render json: { success: false, error: "Invalid password" }
82
- end
83
- end
84
-
85
43
  private
86
44
 
87
45
  def token_service
@@ -93,11 +51,21 @@ module SnfCore
93
51
  end
94
52
 
95
53
  def signup_params
96
- params.require(:user).permit(:first_name, :middle_name, :last_name, :password, :phone_number, :business_name, :tin_number, :business_type)
54
+ params.require(:user).permit(
55
+ :first_name, :middle_name, :last_name, :password,
56
+ :phone_number, :business_name, :tin_number, :business_type,
57
+ :date_of_birth, :nationality, :occupation, :source_of_funds, :kyc_status,
58
+ :gender, :verified_at, :verified_by_id, :address_id
59
+ )
97
60
  end
98
61
 
99
62
  def user_params
100
- signup_params.slice(:first_name, :middle_name, :last_name, :password, :phone_number)
63
+ signup_params.slice(
64
+ :first_name, :middle_name, :last_name, :password,
65
+ :phone_number, :date_of_birth, :nationality,
66
+ :occupation, :source_of_funds, :kyc_status,
67
+ :gender, :verified_at, :verified_by_id, :address_id
68
+ )
101
69
  end
102
70
  end
103
71
  end
@@ -0,0 +1,37 @@
1
+ module SnfCore
2
+ class AccountTransfer < ApplicationRecord
3
+ belongs_to :source_account, polymorphic: true
4
+ belongs_to :destination_account, polymorphic: true
5
+ belongs_to :user
6
+
7
+ enum :status, {
8
+ initiated: 0,
9
+ pending: 1,
10
+ processing: 2,
11
+ completed: 3,
12
+ failed: 4,
13
+ cancelled: 5,
14
+ reversed: 6
15
+ }
16
+
17
+ enum :transfer_type, {
18
+ virtual_to_cbs: 0,
19
+ cbs_to_virtual: 1,
20
+ virtual_to_virtual: 2
21
+ }
22
+
23
+ validates :amount, presence: true, numericality: { greater_than: 0 }
24
+ validates :reference_number, presence: true, uniqueness: true
25
+ validates :status, presence: true
26
+ validates :transfer_type, presence: true
27
+
28
+ before_validation :generate_reference_number, on: :create
29
+
30
+
31
+ private
32
+
33
+ def generate_reference_number
34
+ self.reference_number ||= "TRF-#{SecureRandom.hex(8).upcase}"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ module SnfCore
2
+ class ItemRequest < ApplicationRecord
3
+ belongs_to :user
4
+ belongs_to :product
5
+
6
+ validates :quantity, presence: true, numericality: { greater_than: 0 }
7
+ validates :requested_delivery_date, presence: true
8
+ validates :status, presence: true
9
+ validate :delivery_date_cannot_be_in_past
10
+
11
+ enum :status, {
12
+ pending: 0,
13
+ approved: 1,
14
+ rejected: 2,
15
+ fulfilled: 3,
16
+ cancelled: 4
17
+ }
18
+
19
+ private
20
+
21
+ def delivery_date_cannot_be_in_past
22
+ if requested_delivery_date.present? && requested_delivery_date < Date.current
23
+ errors.add(:requested_delivery_date, "can't be in the past")
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ module SnfCore
2
+ class Quotation < ApplicationRecord
3
+ belongs_to :item_request
4
+
5
+ validates :price, presence: true, numericality: { greater_than: 0 }
6
+ validates :valid_until, presence: true
7
+ validates :delivery_date, presence: true
8
+ validates :status, presence: true
9
+ validate :valid_until_cannot_be_in_past
10
+ validate :delivery_date_cannot_be_in_past
11
+
12
+ before_validation :check_expiration
13
+
14
+ enum :status, {
15
+ pending: 0,
16
+ accepted: 1,
17
+ rejected: 2,
18
+ expired: 3
19
+ }
20
+
21
+ private
22
+
23
+ def check_expiration
24
+ if valid_until.present? && valid_until < DateTime.current && !expired?
25
+ self.status = :expired
26
+ end
27
+ end
28
+
29
+ def valid_until_cannot_be_in_past
30
+ if valid_until.present? && valid_until < DateTime.current && new_record?
31
+ errors.add(:valid_until, "can't be in the past")
32
+ end
33
+ end
34
+
35
+ def delivery_date_cannot_be_in_past
36
+ if delivery_date.present? && delivery_date < Date.current
37
+ errors.add(:delivery_date, "can't be in the past")
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,18 +1,18 @@
1
- module SnfCore
2
- class User < ApplicationRecord
3
- has_secure_password
4
-
5
- has_one :wallet, dependent: :destroy
6
-
7
- validates :first_name, :middle_name, :last_name, :phone_number, presence: true
8
- validates :phone_number, :email, uniqueness: true
9
-
10
- after_create :create_default_wallet
11
-
12
- private
13
-
14
- def create_default_wallet
15
- create_wallet(balance: 0.0, is_active: true)
16
- end
17
- end
18
- end
1
+ module SnfCore
2
+ class User < ApplicationRecord
3
+ has_secure_password
4
+
5
+ belongs_to :verified_by, class_name: "SnfCore::User", optional: true
6
+ belongs_to :address
7
+
8
+ has_one :wallet, dependent: :destroy
9
+
10
+ validates :first_name, :middle_name, :last_name, :phone_number, :date_of_birth, :nationality, presence: true
11
+ validates :phone_number, :email, uniqueness: true
12
+
13
+ enum :kyc_status, { pending: 0, approved: 1, rejected: 2 }
14
+ enum :gender, { male: 0, female: 1 }
15
+
16
+ has_one :virtual_account, dependent: :destroy
17
+ end
18
+ end
@@ -0,0 +1,41 @@
1
+ module SnfCore
2
+ class VirtualAccount < ApplicationRecord
3
+ belongs_to :user
4
+
5
+ enum :interest_type, {
6
+ conventional: 0, # Standard interest type
7
+ interest_free: 1, # No interest (IFB)
8
+ preferential: 2, # Special rates for specific customer segments
9
+ custom: 3 # Negotiated rates for specific cases
10
+ }
11
+
12
+ validates :account_number, presence: true, uniqueness: { case_sensitive: true }
13
+ validates :cbs_account_number, presence: true, uniqueness: true
14
+ validates :balance, presence: true, numericality: { greater_than_or_equal_to: 0 }
15
+ validates :interest_rate, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }
16
+ validates :interest_type, presence: true
17
+ validates :branch_code, presence: true, length: { is: 3 }
18
+ validates :product_scheme, presence: true, length: { is: 1 }
19
+ validates :voucher_type, presence: true, length: { is: 1 }
20
+ validates :active, presence: true, inclusion: { in: [ true, false ] }
21
+
22
+ before_validation :generate_account_number, on: :create
23
+ validate :validate_interest_free_rate
24
+
25
+ private
26
+
27
+ def validate_interest_free_rate
28
+ if interest_free? && !interest_rate.zero?
29
+ errors.add(:interest_rate, "must be 0 for interest-free accounts")
30
+ end
31
+ end
32
+
33
+ def generate_account_number
34
+ return if account_number.present?
35
+
36
+ last_seq = self.class.maximum(:account_number).to_s[-6..-1].to_i
37
+ seq = (last_seq + 1).to_s.rjust(6, "0")
38
+ self.account_number = "#{branch_code}#{product_scheme}#{voucher_type}#{seq}"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,52 @@
1
+ module SnfCore
2
+ class VirtualAccountTransaction < ApplicationRecord
3
+ belongs_to :from_account, class_name: "SnfCore::VirtualAccount", optional: true
4
+ belongs_to :to_account, class_name: "SnfCore::VirtualAccount", optional: true
5
+
6
+ enum :transaction_type, {
7
+ transfer: 0,
8
+ deposit: 1,
9
+ withdrawal: 2
10
+ }
11
+
12
+ enum :status, {
13
+ pending: 0,
14
+ completed: 1,
15
+ failed: 2,
16
+ reversed: 3
17
+ }
18
+
19
+ validates :amount, presence: true, numericality: { greater_than: 0 }
20
+ validates :reference_number, presence: true, uniqueness: true
21
+ validates :transaction_type, presence: true
22
+ validates :status, presence: true
23
+
24
+ validate :validate_accounts
25
+ validate :validate_sufficient_balance, if: -> { amount.present? && transfer_or_withdrawal? }
26
+
27
+ private
28
+
29
+ def validate_sufficient_balance
30
+ return unless from_account
31
+ return if from_account.balance >= amount
32
+
33
+ errors.add(:amount, "Insufficient balance")
34
+ end
35
+
36
+ def validate_accounts
37
+ case transaction_type.to_s
38
+ when "transfer"
39
+ errors.add(:base, "Transfer requires both from and to accounts") unless from_account && to_account
40
+ errors.add(:base, "Cannot transfer to same account") if from_account == to_account
41
+ when "deposit"
42
+ errors.add(:base, "Deposit requires to_account") unless to_account
43
+ when "withdrawal"
44
+ errors.add(:base, "Withdrawal requires from_account") unless from_account
45
+ end
46
+ end
47
+
48
+ def transfer_or_withdrawal?
49
+ %w[transfer withdrawal].include?(transaction_type.to_s)
50
+ end
51
+ end
52
+ end
data/config/routes.rb CHANGED
@@ -1,7 +1,4 @@
1
1
  SnfCore::Engine.routes.draw do
2
2
  post "auth/login"
3
- post "auth/reset_password_request"
4
- post "auth/reset_password"
5
- post "auth/change_password"
6
3
  post "auth/signup"
7
4
  end
@@ -0,0 +1,22 @@
1
+ class CreateSnfCoreVirtualAccounts < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :snf_core_virtual_accounts do |t|
4
+ t.references :user, null: false, foreign_key: { to_table: :snf_core_users }
5
+ t.string :account_number, null: false, limit: 11
6
+ t.string :cbs_account_number, null: false
7
+ t.decimal :balance, null: false, default: 0.0
8
+ t.decimal :interest_rate, null: false, default: 0.0
9
+ t.integer :interest_type, null: false, default: 0
10
+ t.boolean :active, null: false, default: true
11
+ t.string :branch_code, null: false, limit: 3
12
+ t.string :product_scheme, null: false, limit: 1
13
+ t.string :voucher_type, null: false, limit: 1
14
+
15
+ t.timestamps
16
+
17
+ t.index :account_number, unique: true
18
+ t.index :cbs_account_number, unique: true
19
+ t.index [ :branch_code, :product_scheme, :voucher_type ]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ class AddKycAttributesToUser < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :snf_core_users, :date_of_birth, :date, null: false
4
+ add_column :snf_core_users, :nationality, :string, null: false
5
+ add_column :snf_core_users, :occupation, :string
6
+ add_column :snf_core_users, :source_of_funds, :string
7
+ add_column :snf_core_users, :kyc_status, :integer
8
+ add_column :snf_core_users, :gender, :integer
9
+ add_column :snf_core_users, :verified_at, :datetime
10
+ add_column :snf_core_users, :verified_by_id, :bigint
11
+ add_column :snf_core_addresses, :house_number, :string
12
+ add_column :snf_core_users, :address_id, :bigint
13
+
14
+ add_foreign_key :snf_core_users, :snf_core_users, column: :verified_by_id, primary_key: :id, on_delete: :nullify
15
+ add_foreign_key :snf_core_users, :snf_core_addresses, column: :address_id, primary_key: :id
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ class CreateSnfCoreVirtualAccountTransactions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :snf_core_virtual_account_transactions do |t|
4
+ t.references :from_account, foreign_key: { to_table: :snf_core_virtual_accounts }
5
+ t.references :to_account, foreign_key: { to_table: :snf_core_virtual_accounts }
6
+ t.decimal :amount, null: false
7
+ t.integer :transaction_type, null: false
8
+ t.integer :status, null: false, default: 0
9
+ t.string :reference_number, null: false
10
+ t.text :description
11
+
12
+ t.timestamps
13
+
14
+ t.index :reference_number, unique: true
15
+ t.index :created_at
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ class CreateSnfCoreItemRequests < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :snf_core_item_requests do |t|
4
+ t.references :user, null: false, foreign_key: { to_table: :snf_core_users }
5
+ t.references :product, null: false, foreign_key: { to_table: :snf_core_products }
6
+ t.integer :quantity, null: false
7
+ t.date :requested_delivery_date, null: false
8
+ t.text :notes
9
+ t.integer :status, null: false, default: 0
10
+
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ class CreateSnfCoreAccountTransfers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :snf_core_account_transfers do |t|
4
+ t.references :source_account, polymorphic: true, null: false
5
+ t.references :destination_account, polymorphic: true, null: false
6
+ t.references :user, null: false, foreign_key: { to_table: :snf_core_users }
7
+ t.decimal :amount, precision: 15, scale: 2, null: false
8
+ t.string :reference_number, null: false, index: { unique: true }
9
+ t.integer :status, null: false, default: 0
10
+ t.integer :transfer_type, null: false
11
+ t.text :description
12
+ t.text :failure_reason
13
+ t.datetime :completed_at
14
+ t.bigint :reversal_transfer_id
15
+ t.jsonb :cbs_response_data
16
+ t.timestamps
17
+ end
18
+
19
+ add_index :snf_core_account_transfers, :reversal_transfer_id
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ class CreateSnfCoreQuotations < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :snf_core_quotations do |t|
4
+ t.references :item_request, null: false, foreign_key: {to_table: :snf_core_item_requests}
5
+ t.decimal :price , null: false
6
+ t.date :delivery_date, null: false
7
+ t.datetime :valid_until, null: false
8
+ t.integer :status, null: false, default: 0
9
+ t.text :notes
10
+
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
@@ -1,3 +1,3 @@
1
1
  module SnfCore
2
- VERSION = "0.2.94"
2
+ VERSION = "0.2.97"
3
3
  end