fortnox-api 0.9.2 → 1.0.0.rc2

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 (344) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +120 -95
  3. data/{LICENSE.txt → LICENSE.md} +0 -0
  4. data/README.md +245 -248
  5. data/bin/fortnox-setup +211 -0
  6. data/bin/fortnox-update-env +38 -0
  7. data/fortnox.gemspec +29 -0
  8. data/lib/fortnox/auth/thread_local.rb +28 -0
  9. data/lib/fortnox/collection.rb +23 -0
  10. data/lib/fortnox/mappers/country_code.rb +34 -0
  11. data/lib/fortnox/mappers/date.rb +21 -0
  12. data/lib/fortnox/mappers/document_row.rb +12 -0
  13. data/lib/fortnox/mappers/edi_information.rb +15 -0
  14. data/lib/fortnox/mappers/email_information.rb +11 -0
  15. data/lib/fortnox/mappers/invoice_row.rb +14 -0
  16. data/lib/fortnox/mappers/label_references.rb +24 -0
  17. data/lib/fortnox/mappers/order_row.rb +9 -0
  18. data/lib/fortnox/mappers/struct.rb +71 -0
  19. data/lib/fortnox/mappers/struct_array.rb +31 -0
  20. data/lib/fortnox/resource.rb +131 -0
  21. data/lib/fortnox/resources/article.rb +160 -0
  22. data/lib/fortnox/resources/customer.rb +209 -0
  23. data/lib/fortnox/resources/document.rb +202 -0
  24. data/lib/fortnox/resources/invoice.rb +92 -0
  25. data/lib/fortnox/resources/label.rb +15 -0
  26. data/lib/fortnox/resources/order.rb +38 -0
  27. data/lib/fortnox/resources/project.rb +41 -0
  28. data/lib/fortnox/resources/terms_of_payment.rb +23 -0
  29. data/lib/fortnox/resources/unit.rb +26 -0
  30. data/lib/fortnox/struct.rb +24 -0
  31. data/lib/fortnox/structs/default_delivery_types.rb +16 -0
  32. data/lib/fortnox/structs/default_templates.rb +19 -0
  33. data/lib/fortnox/structs/document_row.rb +62 -0
  34. data/lib/fortnox/structs/edi_information.rb +25 -0
  35. data/lib/fortnox/structs/email_information.rb +22 -0
  36. data/lib/fortnox/structs/invoice_row.rb +13 -0
  37. data/lib/fortnox/structs/order_row.rb +10 -0
  38. data/lib/fortnox/types.rb +129 -0
  39. data/lib/fortnox/{api/version.rb → version.rb} +1 -3
  40. data/lib/fortnox.rb +111 -0
  41. metadata +55 -580
  42. data/.codeclimate.yml +0 -20
  43. data/.env.template +0 -7
  44. data/.env.test +0 -11
  45. data/.gitignore +0 -20
  46. data/.rspec +0 -2
  47. data/.rubocop.yml +0 -41
  48. data/.tool-versions +0 -1
  49. data/.travis.yml +0 -32
  50. data/CLAUDE.md +0 -79
  51. data/CONTRIBUTE.md +0 -38
  52. data/DEVELOPER_README.md +0 -69
  53. data/Gemfile +0 -6
  54. data/Guardfile +0 -16
  55. data/Rakefile +0 -140
  56. data/bin/console +0 -22
  57. data/bin/fortnox +0 -285
  58. data/bin/get_tokens +0 -79
  59. data/bin/renew_tokens +0 -28
  60. data/docs/gotchas.md +0 -146
  61. data/fortnox-api.gemspec +0 -50
  62. data/lib/fortnox/api/mappers/article.rb +0 -23
  63. data/lib/fortnox/api/mappers/base/canonical_name_sym.rb +0 -21
  64. data/lib/fortnox/api/mappers/base/from_json.rb +0 -91
  65. data/lib/fortnox/api/mappers/base/to_json.rb +0 -66
  66. data/lib/fortnox/api/mappers/base.rb +0 -30
  67. data/lib/fortnox/api/mappers/customer.rb +0 -27
  68. data/lib/fortnox/api/mappers/default_delivery_types.rb +0 -15
  69. data/lib/fortnox/api/mappers/default_templates.rb +0 -16
  70. data/lib/fortnox/api/mappers/edi_information.rb +0 -23
  71. data/lib/fortnox/api/mappers/email_information.rb +0 -19
  72. data/lib/fortnox/api/mappers/invoice.rb +0 -29
  73. data/lib/fortnox/api/mappers/invoice_row.rb +0 -25
  74. data/lib/fortnox/api/mappers/order.rb +0 -26
  75. data/lib/fortnox/api/mappers/order_row.rb +0 -23
  76. data/lib/fortnox/api/mappers/project.rb +0 -17
  77. data/lib/fortnox/api/mappers/terms_of_payment.rb +0 -17
  78. data/lib/fortnox/api/mappers/unit.rb +0 -17
  79. data/lib/fortnox/api/mappers/value/array.rb +0 -18
  80. data/lib/fortnox/api/mappers/value/country_string.rb +0 -24
  81. data/lib/fortnox/api/mappers/value/date.rb +0 -11
  82. data/lib/fortnox/api/mappers/value/hash.rb +0 -16
  83. data/lib/fortnox/api/mappers/value/identity.rb +0 -18
  84. data/lib/fortnox/api/mappers.rb +0 -21
  85. data/lib/fortnox/api/models/article.rb +0 -134
  86. data/lib/fortnox/api/models/base.rb +0 -128
  87. data/lib/fortnox/api/models/customer.rb +0 -210
  88. data/lib/fortnox/api/models/document.rb +0 -189
  89. data/lib/fortnox/api/models/invoice.rb +0 -87
  90. data/lib/fortnox/api/models/label.rb +0 -19
  91. data/lib/fortnox/api/models/order.rb +0 -27
  92. data/lib/fortnox/api/models/project.rb +0 -42
  93. data/lib/fortnox/api/models/terms_of_payment.rb +0 -28
  94. data/lib/fortnox/api/models/unit.rb +0 -24
  95. data/lib/fortnox/api/models.rb +0 -9
  96. data/lib/fortnox/api/repositories/article.rb +0 -17
  97. data/lib/fortnox/api/repositories/authentication.rb +0 -61
  98. data/lib/fortnox/api/repositories/base/loaders.rb +0 -64
  99. data/lib/fortnox/api/repositories/base/savers.rb +0 -57
  100. data/lib/fortnox/api/repositories/base.rb +0 -93
  101. data/lib/fortnox/api/repositories/customer.rb +0 -17
  102. data/lib/fortnox/api/repositories/invoice.rb +0 -17
  103. data/lib/fortnox/api/repositories/order.rb +0 -17
  104. data/lib/fortnox/api/repositories/project.rb +0 -17
  105. data/lib/fortnox/api/repositories/terms_of_payment.rb +0 -17
  106. data/lib/fortnox/api/repositories/unit.rb +0 -17
  107. data/lib/fortnox/api/repositories.rb +0 -10
  108. data/lib/fortnox/api/request_handling.rb +0 -46
  109. data/lib/fortnox/api/types/default_delivery_types.rb +0 -20
  110. data/lib/fortnox/api/types/default_templates.rb +0 -23
  111. data/lib/fortnox/api/types/defaulted.rb +0 -11
  112. data/lib/fortnox/api/types/document_row.rb +0 -64
  113. data/lib/fortnox/api/types/edi_information.rb +0 -29
  114. data/lib/fortnox/api/types/email_information.rb +0 -26
  115. data/lib/fortnox/api/types/enums.rb +0 -116
  116. data/lib/fortnox/api/types/invoice_row.rb +0 -19
  117. data/lib/fortnox/api/types/model.rb +0 -37
  118. data/lib/fortnox/api/types/nullable.rb +0 -25
  119. data/lib/fortnox/api/types/order_row.rb +0 -16
  120. data/lib/fortnox/api/types/required.rb +0 -13
  121. data/lib/fortnox/api/types/shim/country_string.rb +0 -10
  122. data/lib/fortnox/api/types/sized.rb +0 -33
  123. data/lib/fortnox/api/types.rb +0 -144
  124. data/lib/fortnox/api.rb +0 -62
  125. data/spec/fortnox/api/mappers/article_spec.rb +0 -17
  126. data/spec/fortnox/api/mappers/base/canonical_name_sym_spec.rb +0 -36
  127. data/spec/fortnox/api/mappers/base/from_json_spec.rb +0 -70
  128. data/spec/fortnox/api/mappers/base/to_json_spec.rb +0 -68
  129. data/spec/fortnox/api/mappers/base_spec.rb +0 -154
  130. data/spec/fortnox/api/mappers/contexts/json_conversion.rb +0 -62
  131. data/spec/fortnox/api/mappers/customer_spec.rb +0 -27
  132. data/spec/fortnox/api/mappers/default_delivery_types_spec.rb +0 -14
  133. data/spec/fortnox/api/mappers/edi_information_spec.rb +0 -23
  134. data/spec/fortnox/api/mappers/email_information_spec.rb +0 -19
  135. data/spec/fortnox/api/mappers/examples/mapper.rb +0 -34
  136. data/spec/fortnox/api/mappers/invoice_row_spec.rb +0 -24
  137. data/spec/fortnox/api/mappers/invoice_spec.rb +0 -27
  138. data/spec/fortnox/api/mappers/order_row_spec.rb +0 -21
  139. data/spec/fortnox/api/mappers/order_spec.rb +0 -23
  140. data/spec/fortnox/api/mappers/project_spec.rb +0 -12
  141. data/spec/fortnox/api/mappers/terms_of_payment_spec.rb +0 -16
  142. data/spec/fortnox/api/mappers/unit_spec.rb +0 -56
  143. data/spec/fortnox/api/models/article_spec.rb +0 -9
  144. data/spec/fortnox/api/models/base_spec.rb +0 -117
  145. data/spec/fortnox/api/models/customer_spec.rb +0 -9
  146. data/spec/fortnox/api/models/examples/document_base.rb +0 -15
  147. data/spec/fortnox/api/models/examples/model.rb +0 -22
  148. data/spec/fortnox/api/models/invoice_spec.rb +0 -11
  149. data/spec/fortnox/api/models/order_spec.rb +0 -12
  150. data/spec/fortnox/api/models/project_spec.rb +0 -9
  151. data/spec/fortnox/api/models/terms_of_payment_spec.rb +0 -9
  152. data/spec/fortnox/api/models/unit_spec.rb +0 -33
  153. data/spec/fortnox/api/repositories/article_spec.rb +0 -80
  154. data/spec/fortnox/api/repositories/authentication_spec.rb +0 -103
  155. data/spec/fortnox/api/repositories/base_spec.rb +0 -168
  156. data/spec/fortnox/api/repositories/customer_spec.rb +0 -119
  157. data/spec/fortnox/api/repositories/examples/all.rb +0 -17
  158. data/spec/fortnox/api/repositories/examples/find.rb +0 -84
  159. data/spec/fortnox/api/repositories/examples/only.rb +0 -34
  160. data/spec/fortnox/api/repositories/examples/save.rb +0 -76
  161. data/spec/fortnox/api/repositories/examples/save_with_nested_model.rb +0 -28
  162. data/spec/fortnox/api/repositories/examples/save_with_specially_named_attribute.rb +0 -26
  163. data/spec/fortnox/api/repositories/examples/search.rb +0 -39
  164. data/spec/fortnox/api/repositories/invoice_spec.rb +0 -297
  165. data/spec/fortnox/api/repositories/order_spec.rb +0 -53
  166. data/spec/fortnox/api/repositories/project_spec.rb +0 -36
  167. data/spec/fortnox/api/repositories/terms_of_payment_spec.rb +0 -34
  168. data/spec/fortnox/api/repositories/unit_spec.rb +0 -39
  169. data/spec/fortnox/api/types/account_number_spec.rb +0 -35
  170. data/spec/fortnox/api/types/country_code_spec.rb +0 -42
  171. data/spec/fortnox/api/types/country_spec.rb +0 -67
  172. data/spec/fortnox/api/types/default_delivery_types_spec.rb +0 -12
  173. data/spec/fortnox/api/types/edi_information_spec.rb +0 -15
  174. data/spec/fortnox/api/types/email_information_spec.rb +0 -15
  175. data/spec/fortnox/api/types/email_spec.rb +0 -56
  176. data/spec/fortnox/api/types/enums_spec.rb +0 -17
  177. data/spec/fortnox/api/types/examples/document_row.rb +0 -25
  178. data/spec/fortnox/api/types/examples/enum.rb +0 -55
  179. data/spec/fortnox/api/types/examples/types.rb +0 -11
  180. data/spec/fortnox/api/types/housework_types_spec.rb +0 -149
  181. data/spec/fortnox/api/types/invoice_row_spec.rb +0 -11
  182. data/spec/fortnox/api/types/model_spec.rb +0 -69
  183. data/spec/fortnox/api/types/nullable_spec.rb +0 -79
  184. data/spec/fortnox/api/types/order_row_spec.rb +0 -15
  185. data/spec/fortnox/api/types/required_spec.rb +0 -36
  186. data/spec/fortnox/api/types/sales_account_spec.rb +0 -57
  187. data/spec/fortnox/api/types/sized_spec.rb +0 -76
  188. data/spec/fortnox/api_spec.rb +0 -66
  189. data/spec/spec_helper.rb +0 -35
  190. data/spec/support/helpers/configuration_helper.rb +0 -39
  191. data/spec/support/helpers/repository_helper.rb +0 -10
  192. data/spec/support/helpers.rb +0 -3
  193. data/spec/support/matchers/type/attribute_matcher.rb +0 -40
  194. data/spec/support/matchers/type/enum_matcher.rb +0 -23
  195. data/spec/support/matchers/type/have_account_number_matcher.rb +0 -23
  196. data/spec/support/matchers/type/have_currency_matcher.rb +0 -9
  197. data/spec/support/matchers/type/have_customer_type_matcher.rb +0 -15
  198. data/spec/support/matchers/type/have_default_delivery_type_matcher.rb +0 -9
  199. data/spec/support/matchers/type/have_discount_type_matcher.rb +0 -9
  200. data/spec/support/matchers/type/have_email_matcher.rb +0 -24
  201. data/spec/support/matchers/type/have_housework_type_matcher.rb +0 -9
  202. data/spec/support/matchers/type/have_nullable_date_matcher.rb +0 -60
  203. data/spec/support/matchers/type/have_nullable_matcher.rb +0 -54
  204. data/spec/support/matchers/type/have_nullable_string_matcher.rb +0 -47
  205. data/spec/support/matchers/type/have_sized_float_matcher.rb +0 -10
  206. data/spec/support/matchers/type/have_sized_integer_matcher.rb +0 -10
  207. data/spec/support/matchers/type/have_sized_string_matcher.rb +0 -36
  208. data/spec/support/matchers/type/have_vat_type_matcher.rb +0 -9
  209. data/spec/support/matchers/type/numeric_matcher.rb +0 -52
  210. data/spec/support/matchers/type/require_attribute_matcher.rb +0 -68
  211. data/spec/support/matchers/type/type_matcher.rb +0 -40
  212. data/spec/support/matchers/type.rb +0 -19
  213. data/spec/support/matchers.rb +0 -3
  214. data/spec/support/vcr_setup.rb +0 -25
  215. data/spec/vcr_cassettes/articles/all.yml +0 -67
  216. data/spec/vcr_cassettes/articles/find_by_hash_failure.yml +0 -62
  217. data/spec/vcr_cassettes/articles/find_failure.yml +0 -62
  218. data/spec/vcr_cassettes/articles/find_id_1.yml +0 -63
  219. data/spec/vcr_cassettes/articles/find_new.yml +0 -63
  220. data/spec/vcr_cassettes/articles/limits/quantity_in_stock_min_value.yml +0 -63
  221. data/spec/vcr_cassettes/articles/limits/quantity_in_stock_rounding_positive_value.yml +0 -63
  222. data/spec/vcr_cassettes/articles/multi_param_find_by_hash.yml +0 -62
  223. data/spec/vcr_cassettes/articles/save_new.yml +0 -63
  224. data/spec/vcr_cassettes/articles/save_old.yml +0 -63
  225. data/spec/vcr_cassettes/articles/save_with_specially_named_attribute.yml +0 -63
  226. data/spec/vcr_cassettes/articles/search_by_name.yml +0 -65
  227. data/spec/vcr_cassettes/articles/search_miss.yml +0 -62
  228. data/spec/vcr_cassettes/articles/search_with_special_char.yml +0 -62
  229. data/spec/vcr_cassettes/articles/single_param_find_by_hash.yml +0 -62
  230. data/spec/vcr_cassettes/authentication/expired_token.yml +0 -54
  231. data/spec/vcr_cassettes/authentication/invalid_authorization.yml +0 -57
  232. data/spec/vcr_cassettes/authentication/invalid_refresh_token.yml +0 -58
  233. data/spec/vcr_cassettes/authentication/valid_request.yml +0 -63
  234. data/spec/vcr_cassettes/customers/all.yml +0 -69
  235. data/spec/vcr_cassettes/customers/find_by_hash_failure.yml +0 -62
  236. data/spec/vcr_cassettes/customers/find_failure.yml +0 -62
  237. data/spec/vcr_cassettes/customers/find_id_1.yml +0 -64
  238. data/spec/vcr_cassettes/customers/find_new.yml +0 -63
  239. data/spec/vcr_cassettes/customers/find_with_sales_account.yml +0 -63
  240. data/spec/vcr_cassettes/customers/multi_param_find_by_hash.yml +0 -63
  241. data/spec/vcr_cassettes/customers/save_new.yml +0 -63
  242. data/spec/vcr_cassettes/customers/save_new_with_country_code_SE.yml +0 -64
  243. data/spec/vcr_cassettes/customers/save_new_with_idn_email.yml +0 -67
  244. data/spec/vcr_cassettes/customers/save_new_with_sales_account.yml +0 -63
  245. data/spec/vcr_cassettes/customers/save_old.yml +0 -63
  246. data/spec/vcr_cassettes/customers/save_with_specially_named_attribute.yml +0 -63
  247. data/spec/vcr_cassettes/customers/search_by_name.yml +0 -64
  248. data/spec/vcr_cassettes/customers/search_miss.yml +0 -62
  249. data/spec/vcr_cassettes/customers/search_with_special_char.yml +0 -62
  250. data/spec/vcr_cassettes/customers/single_param_find_by_hash.yml +0 -64
  251. data/spec/vcr_cassettes/invoices/all.yml +0 -96
  252. data/spec/vcr_cassettes/invoices/filter_hit.yml +0 -64
  253. data/spec/vcr_cassettes/invoices/filter_invalid.yml +0 -60
  254. data/spec/vcr_cassettes/invoices/find_by_hash_failure.yml +0 -62
  255. data/spec/vcr_cassettes/invoices/find_failure.yml +0 -62
  256. data/spec/vcr_cassettes/invoices/find_id_1.yml +0 -65
  257. data/spec/vcr_cassettes/invoices/find_new.yml +0 -65
  258. data/spec/vcr_cassettes/invoices/multi_param_find_by_hash.yml +0 -63
  259. data/spec/vcr_cassettes/invoices/row_delivered_quantity_decimals.yml +0 -65
  260. data/spec/vcr_cassettes/invoices/row_delivered_quantity_decimals_round_up.yml +0 -65
  261. data/spec/vcr_cassettes/invoices/row_description_limit.yml +0 -65
  262. data/spec/vcr_cassettes/invoices/row_price_limit.yml +0 -65
  263. data/spec/vcr_cassettes/invoices/row_price_limit_round_up.yml +0 -65
  264. data/spec/vcr_cassettes/invoices/save_new.yml +0 -65
  265. data/spec/vcr_cassettes/invoices/save_new_with_comments.yml +0 -65
  266. data/spec/vcr_cassettes/invoices/save_new_with_country.yml +0 -65
  267. data/spec/vcr_cassettes/invoices/save_new_with_country_GB.yml +0 -66
  268. data/spec/vcr_cassettes/invoices/save_new_with_country_Norge.yml +0 -65
  269. data/spec/vcr_cassettes/invoices/save_new_with_country_Norway.yml +0 -65
  270. data/spec/vcr_cassettes/invoices/save_new_with_country_Sverige.yml +0 -65
  271. data/spec/vcr_cassettes/invoices/save_new_with_country_VA.yml +0 -66
  272. data/spec/vcr_cassettes/invoices/save_new_with_country_VI.yml +0 -66
  273. data/spec/vcr_cassettes/invoices/save_new_with_country_empty_string.yml +0 -65
  274. data/spec/vcr_cassettes/invoices/save_new_with_country_nil.yml +0 -65
  275. data/spec/vcr_cassettes/invoices/save_new_with_unsaved_parent.yml +0 -65
  276. data/spec/vcr_cassettes/invoices/save_old.yml +0 -65
  277. data/spec/vcr_cassettes/invoices/save_old_with_empty_comments.yml +0 -65
  278. data/spec/vcr_cassettes/invoices/save_old_with_empty_country.yml +0 -65
  279. data/spec/vcr_cassettes/invoices/save_old_with_nil_comments.yml +0 -65
  280. data/spec/vcr_cassettes/invoices/save_old_with_nil_country.yml +0 -65
  281. data/spec/vcr_cassettes/invoices/save_with_nested_model.yml +0 -65
  282. data/spec/vcr_cassettes/invoices/save_with_specially_named_attribute.yml +0 -65
  283. data/spec/vcr_cassettes/invoices/search_by_name.yml +0 -63
  284. data/spec/vcr_cassettes/invoices/search_miss.yml +0 -62
  285. data/spec/vcr_cassettes/invoices/search_with_special_char.yml +0 -62
  286. data/spec/vcr_cassettes/invoices/single_param_find_by_hash.yml +0 -64
  287. data/spec/vcr_cassettes/orders/all.yml +0 -69
  288. data/spec/vcr_cassettes/orders/filter_hit.yml +0 -64
  289. data/spec/vcr_cassettes/orders/filter_invalid.yml +0 -60
  290. data/spec/vcr_cassettes/orders/find_by_hash_failure.yml +0 -62
  291. data/spec/vcr_cassettes/orders/find_failure.yml +0 -62
  292. data/spec/vcr_cassettes/orders/find_id_1.yml +0 -67
  293. data/spec/vcr_cassettes/orders/find_new.yml +0 -65
  294. data/spec/vcr_cassettes/orders/housework_invalid_tax_reduction_type.yml +0 -61
  295. data/spec/vcr_cassettes/orders/housework_othercoses_invalid.yml +0 -61
  296. data/spec/vcr_cassettes/orders/housework_type_babysitting.yml +0 -65
  297. data/spec/vcr_cassettes/orders/housework_type_cleaning.yml +0 -65
  298. data/spec/vcr_cassettes/orders/housework_type_construction.yml +0 -65
  299. data/spec/vcr_cassettes/orders/housework_type_cooking.yml +0 -61
  300. data/spec/vcr_cassettes/orders/housework_type_electricity.yml +0 -65
  301. data/spec/vcr_cassettes/orders/housework_type_gardening.yml +0 -65
  302. data/spec/vcr_cassettes/orders/housework_type_glassmetalwork.yml +0 -65
  303. data/spec/vcr_cassettes/orders/housework_type_grounddrainagework.yml +0 -65
  304. data/spec/vcr_cassettes/orders/housework_type_hvac.yml +0 -65
  305. data/spec/vcr_cassettes/orders/housework_type_itservices.yml +0 -65
  306. data/spec/vcr_cassettes/orders/housework_type_majorappliancerepair.yml +0 -65
  307. data/spec/vcr_cassettes/orders/housework_type_masonry.yml +0 -65
  308. data/spec/vcr_cassettes/orders/housework_type_movingservices.yml +0 -65
  309. data/spec/vcr_cassettes/orders/housework_type_othercare.yml +0 -65
  310. data/spec/vcr_cassettes/orders/housework_type_othercosts.yml +0 -65
  311. data/spec/vcr_cassettes/orders/housework_type_paintingwallpapering.yml +0 -65
  312. data/spec/vcr_cassettes/orders/housework_type_snowplowing.yml +0 -65
  313. data/spec/vcr_cassettes/orders/housework_type_textileclothing.yml +0 -65
  314. data/spec/vcr_cassettes/orders/housework_type_tutoring.yml +0 -61
  315. data/spec/vcr_cassettes/orders/multi_param_find_by_hash.yml +0 -63
  316. data/spec/vcr_cassettes/orders/save_new.yml +0 -65
  317. data/spec/vcr_cassettes/orders/save_old.yml +0 -65
  318. data/spec/vcr_cassettes/orders/save_with_nested_model.yml +0 -65
  319. data/spec/vcr_cassettes/orders/search_by_name.yml +0 -63
  320. data/spec/vcr_cassettes/orders/search_miss.yml +0 -62
  321. data/spec/vcr_cassettes/orders/search_with_special_char.yml +0 -62
  322. data/spec/vcr_cassettes/orders/single_param_find_by_hash.yml +0 -64
  323. data/spec/vcr_cassettes/projects/all.yml +0 -64
  324. data/spec/vcr_cassettes/projects/find_by_hash_failure.yml +0 -62
  325. data/spec/vcr_cassettes/projects/find_failure.yml +0 -62
  326. data/spec/vcr_cassettes/projects/find_id_1.yml +0 -63
  327. data/spec/vcr_cassettes/projects/find_new.yml +0 -63
  328. data/spec/vcr_cassettes/projects/multi_param_find_by_hash.yml +0 -64
  329. data/spec/vcr_cassettes/projects/save_new.yml +0 -63
  330. data/spec/vcr_cassettes/projects/save_old.yml +0 -63
  331. data/spec/vcr_cassettes/projects/single_param_find_by_hash.yml +0 -63
  332. data/spec/vcr_cassettes/termsofpayments/all.yml +0 -68
  333. data/spec/vcr_cassettes/termsofpayments/find_failure.yml +0 -62
  334. data/spec/vcr_cassettes/termsofpayments/find_id_1.yml +0 -62
  335. data/spec/vcr_cassettes/termsofpayments/find_new.yml +0 -63
  336. data/spec/vcr_cassettes/termsofpayments/save_new.yml +0 -63
  337. data/spec/vcr_cassettes/termsofpayments/save_old.yml +0 -63
  338. data/spec/vcr_cassettes/units/all.yml +0 -64
  339. data/spec/vcr_cassettes/units/find_failure.yml +0 -62
  340. data/spec/vcr_cassettes/units/find_id_1.yml +0 -63
  341. data/spec/vcr_cassettes/units/find_new.yml +0 -63
  342. data/spec/vcr_cassettes/units/save_new.yml +0 -63
  343. data/spec/vcr_cassettes/units/save_old.yml +0 -63
  344. data/spec/vcr_cassettes/units/save_with_specially_named_attribute.yml +0 -63
data/bin/fortnox-setup ADDED
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
5
+
6
+ require 'base64'
7
+ require 'cgi'
8
+ require 'json'
9
+ require 'rbconfig'
10
+ require 'securerandom'
11
+ require 'socket'
12
+ require 'uri'
13
+ require 'faraday'
14
+ require 'fortnox'
15
+
16
+ OAUTH_ENDPOINT = 'https://apps.fortnox.se/oauth-v1'
17
+ LOCAL_PORT = 4242
18
+ LOCAL_REDIRECT_URI = "http://localhost:#{LOCAL_PORT}".freeze
19
+
20
+ def prompt(message, default: nil)
21
+ if default
22
+ print "#{message} [#{default}]: "
23
+ else
24
+ print "#{message}: "
25
+ end
26
+ input = $stdin.gets.chomp
27
+ input.empty? && default ? default : input
28
+ end
29
+
30
+ def open_browser(url)
31
+ cmd = case RbConfig::CONFIG['host_os']
32
+ when /mswin|mingw|cygwin/ then 'start'
33
+ when /darwin/ then 'open'
34
+ when /linux|bsd/ then 'xdg-open'
35
+ end
36
+
37
+ if cmd
38
+ system "#{cmd} \"#{url}\""
39
+ else
40
+ puts 'Could not open browser automatically. Please open this URL manually:'
41
+ puts url
42
+ end
43
+ end
44
+
45
+ def start_local_server(port)
46
+ server = TCPServer.new(port)
47
+ client = server.accept
48
+ request = client.gets
49
+ _, path, = request.split
50
+
51
+ client.puts "HTTP/1.1 200\r\n\r\n"
52
+ client.puts '<html><body>'
53
+ client.puts '<h1>Authorization received</h1>'
54
+ client.puts '<p>You can close this tab and return to the terminal.</p>'
55
+ client.puts '</body></html>'
56
+ client.close
57
+ server.close
58
+
59
+ URI.decode_www_form(path[2..]).to_h.transform_keys(&:to_sym)
60
+ end
61
+
62
+ def get_auth_code_from_server(port, nonce)
63
+ response = start_local_server(port)
64
+
65
+ if response[:error]
66
+ puts "\nFortnox returned an error: #{response[:error]}"
67
+ puts " #{response[:description]}" if response[:description]
68
+ exit 1
69
+ end
70
+
71
+ normalized_state = CGI.unescape(response[:state]).gsub(' ', '+')
72
+ if normalized_state != nonce
73
+ puts "\nSecurity error: the state parameter returned from Fortnox did not match."
74
+ puts 'This could indicate a replay attack. Aborting.'
75
+ exit 1
76
+ end
77
+
78
+ response[:code]
79
+ rescue SocketError, Errno::EADDRINUSE => e
80
+ puts "\nCould not start local server (#{e.message})."
81
+ auth_code_manually
82
+ end
83
+
84
+ def auth_code_manually
85
+ puts
86
+ puts "After granting access you will be redirected. Copy the 'code' parameter"
87
+ puts 'from the URL you were redirected to.'
88
+ puts
89
+ prompt('Authorization code')
90
+ end
91
+
92
+ def decode_jwt_payload(jwt)
93
+ payload = jwt.split('.')[1]
94
+ # JWTs are base64url-encoded without padding (RFC 7515), but Ruby's
95
+ # urlsafe_decode64 is strict about padding before 3.2 — pad to a
96
+ # multiple of 4 chars so it accepts the input on 3.1.
97
+ payload += '=' * ((4 - (payload.size % 4)) % 4)
98
+ JSON.parse(Base64.urlsafe_decode64(payload))
99
+ end
100
+
101
+ # --- Main ---
102
+
103
+ puts 'Fortnox Setup'
104
+ puts '============='
105
+ puts
106
+ puts 'This script will authorize your Fortnox app and retrieve your tenant ID.'
107
+ puts 'You only need to do this once per Fortnox account.'
108
+ puts
109
+
110
+ client_id = prompt('Client ID')
111
+ client_secret = prompt('Client secret')
112
+
113
+ puts
114
+ puts 'Available scopes:'
115
+ Fortnox.scopes.each do |scope, resources|
116
+ resource_names = resources.map { |r| r.name.split('::').last }.join(', ')
117
+ puts " #{scope.ljust(10)} → #{resource_names}"
118
+ end
119
+ puts
120
+ puts 'These must match the scopes configured on your Fortnox app in the developer'
121
+ puts 'portal. Other Fortnox scopes (salary, bookkeeping, etc.) can be added manually.'
122
+ puts
123
+
124
+ scopes = loop do
125
+ input = prompt("Enter scopes (space-separated, or 'all')").strip
126
+ break Fortnox.scopes.keys.join(' ') if input == 'all'
127
+ break input unless input.empty?
128
+
129
+ puts 'Please enter at least one scope.'
130
+ end
131
+
132
+ puts
133
+ puts 'The script can catch the authorization response automatically using a'
134
+ puts "local server. This requires your Fortnox app's redirect URI to be set to"
135
+ puts LOCAL_REDIRECT_URI
136
+ puts
137
+ use_local = prompt('Use local server?', default: 'Y')
138
+ use_local_server = use_local.downcase.start_with?('y')
139
+
140
+ redirect_uri = use_local_server ? LOCAL_REDIRECT_URI : prompt('Redirect URI')
141
+
142
+ nonce = SecureRandom.base64
143
+
144
+ auth_params = URI.encode_www_form(
145
+ client_id: client_id,
146
+ redirect_uri: redirect_uri,
147
+ scope: scopes,
148
+ state: nonce,
149
+ access_type: 'offline',
150
+ response_type: 'code',
151
+ account_type: 'service'
152
+ )
153
+
154
+ authorize_url = "#{OAUTH_ENDPOINT}/auth?#{auth_params}"
155
+
156
+ puts
157
+ puts 'Opening browser for Fortnox authorization...'
158
+
159
+ open_browser(authorize_url)
160
+
161
+ if use_local_server
162
+ puts
163
+ puts 'Waiting for Fortnox to redirect back...'
164
+ authorization_code = get_auth_code_from_server(LOCAL_PORT, nonce)
165
+ else
166
+ authorization_code = auth_code_manually
167
+ end
168
+
169
+ credentials = Base64.strict_encode64("#{client_id}:#{client_secret}")
170
+
171
+ response = Faraday.post("#{OAUTH_ENDPOINT}/token") do |req|
172
+ req.headers['Authorization'] = "Basic #{credentials}"
173
+ req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
174
+ req.body = URI.encode_www_form(
175
+ grant_type: 'authorization_code',
176
+ code: authorization_code,
177
+ redirect_uri: redirect_uri
178
+ )
179
+ end
180
+
181
+ parsed = JSON.parse(response.body)
182
+
183
+ unless response.success?
184
+ puts "\nSomething went wrong."
185
+ puts "Response code: #{response.status}"
186
+ puts "Error: #{parsed['error_description'] || parsed['error'] || response.body}"
187
+ exit 1
188
+ end
189
+
190
+ access_token = parsed['access_token']
191
+
192
+ tenant_id = decode_jwt_payload(access_token)['tenantId']
193
+
194
+ puts
195
+ puts 'Setup complete!'
196
+ puts
197
+ puts "Tenant ID: #{tenant_id}"
198
+ puts "Access token: #{access_token}"
199
+ puts "Expires in: #{parsed['expires_in']} seconds"
200
+ puts "Scopes: #{parsed['scope']}"
201
+ puts
202
+ puts 'Store your tenant ID somewhere safe. Together with your client ID and'
203
+ puts 'client secret, you can use it to request new access tokens at any time:'
204
+ puts
205
+ puts ' Fortnox.request_access_token('
206
+ puts " client_id: '#{client_id}',"
207
+ puts " client_secret: '<your-secret>',"
208
+ puts " tenant_id: '#{tenant_id}'"
209
+ puts ' )'
210
+
211
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'dotenv'
6
+ require 'fortnox'
7
+
8
+ env_file = ARGV[0] || '.env'
9
+
10
+ unless File.exist?(env_file)
11
+ puts "#{env_file} not found."
12
+ puts 'Create it with FORTNOX_CLIENT_ID, FORTNOX_CLIENT_SECRET, and FORTNOX_TENANT_ID.'
13
+ exit 1
14
+ end
15
+
16
+ Dotenv.load(env_file)
17
+
18
+ client_id = ENV.fetch('FORTNOX_CLIENT_ID') { abort 'Missing FORTNOX_CLIENT_ID' }
19
+ client_secret = ENV.fetch('FORTNOX_CLIENT_SECRET') { abort 'Missing FORTNOX_CLIENT_SECRET' }
20
+ tenant_id = ENV.fetch('FORTNOX_TENANT_ID') { abort 'Missing FORTNOX_TENANT_ID' }
21
+
22
+ token = Fortnox.request_access_token(
23
+ client_id: client_id,
24
+ client_secret: client_secret,
25
+ tenant_id: tenant_id
26
+ )
27
+
28
+ text = File.read(env_file)
29
+
30
+ if text.match?(/^FORTNOX_ACCESS_TOKEN=/)
31
+ text.gsub!(/^FORTNOX_ACCESS_TOKEN=.*$/, "FORTNOX_ACCESS_TOKEN=#{token}")
32
+ else
33
+ text += "\nFORTNOX_ACCESS_TOKEN=#{token}\n"
34
+ end
35
+
36
+ File.write(env_file, text)
37
+
38
+ puts "Access token written to #{env_file}"
data/fortnox.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'fortnox/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'fortnox-api'
9
+ spec.authors = ['Jonas Schubert Erlandsson', 'Hannes Elvemyr', 'Felix Holmgren', 'Mike Eirih']
10
+ spec.email = ['info@accodeing.com']
11
+ spec.license = 'LGPL-3.0'
12
+ spec.version = Fortnox::VERSION.dup
13
+
14
+ spec.summary = 'Fortnox F3 REST API library, based on rest-easy.'
15
+ spec.description = spec.summary
16
+ spec.homepage = 'https://github.com/accodeing/fortnox'
17
+ spec.files = Dir['CHANGELOG.md', 'LICENSE.md', 'README.md', 'fortnox.gemspec', 'lib/**/*']
18
+ spec.bindir = 'bin'
19
+ spec.executables = ['fortnox-setup', 'fortnox-update-env']
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.required_ruby_version = '>= 3.1.0'
23
+
24
+ spec.add_dependency 'countries', '~> 7.1'
25
+ spec.add_dependency 'dry-struct', '~> 1.5'
26
+ spec.add_dependency 'rest-easy', '~> 1.0.0'
27
+
28
+ spec.metadata['rubygems_mfa_required'] = 'true'
29
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fortnox
4
+ module Auth
5
+ # Bearer-token authentication isolated between threads, so concurrent
6
+ # contexts (Sidekiq workers, Puma threads) can use different tokens
7
+ # without leaking across them.
8
+ class ThreadLocal
9
+ THREAD_LOCAL_KEY = :fortnox_access_token
10
+
11
+ def apply(request)
12
+ request.headers['Authorization'] = "Bearer #{token!}"
13
+ end
14
+
15
+ def on_rejected(response)
16
+ raise Fortnox::RequestError, response
17
+ end
18
+
19
+ private
20
+
21
+ def token!
22
+ Thread.current[THREAD_LOCAL_KEY] ||
23
+ raise(Fortnox::MissingAccessToken,
24
+ 'No access token set for the current thread. Set one with: Fortnox.access_token = token')
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Fortnox
6
+ # An iterable wrapper around a list of resource instances that also carries
7
+ # the pagination metadata Fortnox returns alongside collection responses.
8
+ class Collection
9
+ include Enumerable
10
+ extend Forwardable
11
+
12
+ def_delegators :@items, :each, :first, :last, :size, :length, :empty?, :[], :to_a
13
+
14
+ attr_reader :total, :pages, :current_page
15
+
16
+ def initialize(items, total: nil, pages: nil, current_page: nil)
17
+ @items = items
18
+ @total = total
19
+ @pages = pages
20
+ @current_page = current_page
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'countries'
4
+
5
+ module Fortnox
6
+ module Mappers
7
+ module CountryCode
8
+ ISO3166.configure { |config| config.locales = [:en, :sv] }
9
+
10
+ def self.parse(country)
11
+ return '' if country.nil? || country == ''
12
+
13
+ # Fortnox only supports Swedish translation of Sweden
14
+ return 'SE' if country =~ /^s(e$|we|ve)/i
15
+
16
+ country = ::ISO3166::Country[country] ||
17
+ ::ISO3166::Country.find_country_by_iso_short_name(country) ||
18
+ ::ISO3166::Country.find_country_by_translated_names(country)
19
+
20
+ raise Fortnox::AttributeError, '"Country" violates constraints' if country.nil?
21
+
22
+ country.alpha2
23
+ end
24
+
25
+ def self.serialise(country_code)
26
+ return '' if country_code.nil? || country_code == ''
27
+
28
+ return 'Sverige' if country_code == 'SE'
29
+
30
+ ::ISO3166::Country.new(country_code).translations['en']
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Fortnox
6
+ module Mappers
7
+ module Date
8
+ def self.parse(date_string)
9
+ return nil if date_string.nil? || (date_string == '')
10
+
11
+ ::Date.parse(date_string)
12
+ end
13
+
14
+ def self.serialise(date)
15
+ return nil if date.nil?
16
+
17
+ date.strftime('%F')
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fortnox
4
+ module Mappers
5
+ class DocumentRow < Struct
6
+ struct Structs::DocumentRow
7
+ overrides housework: 'HouseWork',
8
+ housework_hours_to_report: 'HouseWorkHoursToReport',
9
+ housework_type: 'HouseWorkType'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fortnox
4
+ module Mappers
5
+ class EDIInformation < Struct
6
+ struct Structs::EDIInformation
7
+ overrides edi_global_location_number: 'EDIGlobalLocationNumber',
8
+ edi_global_location_number_delivery: 'EDIGlobalLocationNumberDelivery',
9
+ edi_invoice_extra1: 'EDIInvoiceExtra1',
10
+ edi_invoice_extra2: 'EDIInvoiceExtra2',
11
+ edi_our_electronic_reference: 'EDIOurElectronicReference',
12
+ edi_your_electronic_reference: 'EDIYourElectronicReference'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fortnox
4
+ module Mappers
5
+ class EmailInformation < Struct
6
+ struct Structs::EmailInformation
7
+ overrides email_address_cc: 'EmailAddressCC',
8
+ email_address_bcc: 'EmailAddressBCC'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fortnox
4
+ module Mappers
5
+ class InvoiceRow < DocumentRow
6
+ struct Structs::InvoiceRow
7
+ overrides housework: 'HouseWork',
8
+ housework_hours_to_report: 'HouseWorkHoursToReport',
9
+ housework_type: 'HouseWorkType',
10
+ price_excluding_vat: 'PriceExcludingVAT',
11
+ total_excluding_vat: 'TotalExcludingVAT'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fortnox
4
+ module Mappers
5
+ module LabelReferences
6
+ def self.parse(labels)
7
+ return [] unless labels.is_a?(Array)
8
+
9
+ labels.map do |label|
10
+ Fortnox::Label.parse({ 'Label' => label })
11
+ end
12
+ end
13
+
14
+ def self.serialise(labels)
15
+ return [] if labels.nil?
16
+ return [] unless labels.is_a?(Array)
17
+
18
+ labels.map do |label|
19
+ { 'Id' => label.id }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fortnox
4
+ module Mappers
5
+ class OrderRow < DocumentRow
6
+ struct Structs::OrderRow
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fortnox
4
+ module Mappers
5
+ # Base class for declarative struct mappers. Subclasses declare the target
6
+ # struct and any API key overrides (acronyms like "EDIInformation" that the
7
+ # PascalCase convention can't derive). Read-only attributes are excluded
8
+ # from serialisation.
9
+ #
10
+ # class EmailInformation < Struct
11
+ # struct Structs::EmailInformation
12
+ # overrides email_address_cc: 'EmailAddressCC'
13
+ # end
14
+ class Struct
15
+ CONVENTION = RestEasy::Conventions::PascalCase.new
16
+
17
+ class << self
18
+ def for(struct_class)
19
+ Class.new(self) { struct struct_class }
20
+ end
21
+
22
+ def struct(klass)
23
+ @struct_class = klass
24
+ end
25
+
26
+ def overrides(map)
27
+ @overrides = map
28
+ end
29
+
30
+ def parse(data)
31
+ return nil if data.nil?
32
+
33
+ attributes = data.transform_keys do |api_key|
34
+ api_to_model_map[api_key] || CONVENTION.parse(api_key)
35
+ end
36
+ struct_class.new(attributes)
37
+ end
38
+
39
+ def serialise(struct)
40
+ return nil if struct.nil?
41
+
42
+ excluded = if struct.class.respond_to?(:read_only_attributes)
43
+ struct.class.read_only_attributes
44
+ else
45
+ []
46
+ end
47
+ struct.to_h.except(*excluded).transform_keys do |key|
48
+ model_to_api_map[key] || CONVENTION.serialise(key)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :struct_class
55
+
56
+ def model_to_api_map
57
+ parent = if superclass.respond_to?(:send, true) && superclass != Struct
58
+ superclass.send(:model_to_api_map)
59
+ else
60
+ {}
61
+ end
62
+ @overrides ? parent.merge(@overrides) : parent
63
+ end
64
+
65
+ def api_to_model_map
66
+ model_to_api_map.invert
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fortnox
4
+ module Mappers
5
+ module StructArray
6
+ def self.for(mapper)
7
+ Handler.new(mapper)
8
+ end
9
+
10
+ class Handler
11
+ def initialize(mapper)
12
+ @mapper = mapper
13
+ end
14
+
15
+ def parse(data)
16
+ return [] if data.nil?
17
+ raise Fortnox::AttributeError, "Expected Array, got #{data.class}" unless data.is_a?(Array)
18
+
19
+ data.map { |item| @mapper.parse(item) }
20
+ end
21
+
22
+ def serialise(structs)
23
+ return [] if structs.nil?
24
+ raise Fortnox::AttributeError, "Expected Array, got #{structs.class}" unless structs.is_a?(Array)
25
+
26
+ structs.map { |struct| @mapper.serialise(struct) }
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end