diff --git a/.ruby-version b/.ruby-version index 276cbf9e2..338a5b5d8 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.0 +2.6.6 diff --git a/Gemfile b/Gemfile index ac23ef3eb..ba0ce65e2 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,7 @@ source 'https://rubygems.org' gemspec gem 'simplecov', :require => false +gem 'rspec_junit_formatter' gem 'pry-nav' gem 'pry-rescue' @@ -11,5 +12,5 @@ gem 'tzinfo', '1.2.5' # gem 'tzinfo', '2.0.0' # required for CircleCI to build properly with ruby 1.9.3 -gem 'json', '~> 1.8.3' -gem 'rack', '~> 1.6.4' +gem 'json', '~> 2.3.0' +gem 'rack', '~> 2.1.4' diff --git a/README.md b/README.md index 2797000ce..672fb4241 100644 --- a/README.md +++ b/README.md @@ -60,14 +60,36 @@ This gem is built for ruby 1.9.x+, checkout the [1-8-stable](https://github.com/ ## Configuration -Not sure how to find your account id? Search for "web service preferences" in the NetSuite global search. +The most important thing you'll need is your NetSuite account ID. Not sure how to find your account id? [Here's a guide.](http://mikebian.co/find-netsuite-web-services-account-number/) + +For most use-cases, the following configuration will be sufficient: + +```ruby +NetSuite.configure do + reset! + + account 'TSTDRV1576318' + api_version '2018_2' + + email 'email@example.com' + password 'password' + role 10 + + # use `NetSuite::Utilities.data_center_url('TSTDRV1576318')` to retrieve the URL + # you'll want to do this in a background process and strip the protocol out of the return string + wsdl_domain 'tstdrv1576318.suitetalk.api.netsuite.com' +end +``` + +The `wsdl_domain` configuration is most important. Note that if you use `wsdl` or other configuration options below, you'll want to look at the configuration source to understand more about how the different options interact with each other. Some of the configuration options will mutate the state of other options. + +Here's the various options that are are available for configuration: ```ruby NetSuite.configure do reset! - # optional, defaults to 2011_2 - api_version '2012_1' + api_version '2018_2' # optionally specify full wsdl URL (to switch to sandbox, for example) wsdl "https://webservices.sandbox.netsuite.com/wsdl/v#{api_version}_0/netsuite.wsdl" @@ -79,21 +101,24 @@ NetSuite.configure do # construct the full wsdl location - e.g. "https://#{wsdl_domain}/wsdl/v#{api_version}_0/netsuite.wsdl" wsdl_domain "webservices.na2.netsuite.com" - # or specify the sandbox flag if you don't want to deal with specifying a full URL - sandbox true - # often the netsuite servers will hang which would cause a timeout exception to be raised - # if you don't mind waiting (e.g. processing NS via DJ), increasing the timeout should fix the issue - read_timeout 100000 + # if you don't mind waiting (e.g. processing NS via a background worker), increasing the timeout should fix the issue + read_timeout 100_000 # you can specify a file or file descriptor to send the log output to (defaults to STDOUT) + # If using within a Rails app, consider setting to `Rails.logger` to leverage existing + # application-level log configuration log File.join(Rails.root, 'log/netsuite.log') - # login information - email 'email@domain.com' - password 'password' + # Defaults to :debug level logging for Savon API calls. Decrease the verbosity + # by setting log_level to `:info`, for example + # log_level :debug + + # password-based login information + email 'email@domain.com' + password 'password' account '12345' - role 1111 + role 1111 # optional, ensures that read-only fields don't cause API errors soap_header 'platformMsgs:preferences' => { diff --git a/circle.yml b/circle.yml index c1f128423..664e435a9 100644 --- a/circle.yml +++ b/circle.yml @@ -1,17 +1,36 @@ -# https://leonid.shevtsov.me/post/multiple-rubies-on-circleci/ +version: 2.1 -machine: - environment: - RUBY_VERSIONS: 2.0.0,2.1.10,2.2.9,2.3.7,2.4.4,2.5.1,2.6.1 +orbs: + # orbs are basically bundles of pre-written build scripts that work for common cases + # https://github.com/CircleCI-Public/ruby-orb + ruby: circleci/ruby@1.1 -dependencies: - override: - - gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB - - rvm get head - - rvm install $RUBY_VERSIONS - - rvm $RUBY_VERSIONS --verbose do gem install bundler -v 1.17.3 - - rvm $RUBY_VERSIONS --verbose do bundle install +jobs: + # skipping build step because Gemfile.lock is not included in the source + # this makes the bundler caching step a noop + test: + parameters: + ruby-version: + type: string + docker: + - image: cimg/ruby:<< parameters.ruby-version >> + steps: + - checkout + - ruby/install-deps: + bundler-version: '1.17.2' + with-cache: false + - ruby/rspec-test -test: - override: - - rvm $RUBY_VERSIONS --verbose do bundle exec rspec spec +# strangely, there seems to be very little documentation about exactly how martix builds work. +# By defining a param inside your job definition, Circle CI will automatically spawn a job for +# unique param value passed via `matrix`. Neat! +# https://circleci.com/blog/circleci-matrix-jobs/ +workflows: + build_and_test: + jobs: + - test: + matrix: + parameters: + # https://github.com/CircleCI-Public/cimg-ruby + # only supports the last three ruby versions + ruby-version: ["2.5", "2.6", "2.7"] \ No newline at end of file diff --git a/lib/netsuite.rb b/lib/netsuite.rb index 19aa1a2ef..b6bcba512 100644 --- a/lib/netsuite.rb +++ b/lib/netsuite.rb @@ -112,6 +112,8 @@ module Records autoload :CustomerAddressbook, 'netsuite/records/customer_addressbook' autoload :CustomerAddressbookList, 'netsuite/records/customer_addressbook_list' autoload :CustomerCategory, 'netsuite/records/customer_category' + autoload :CustomerCreditCards, 'netsuite/records/customer_credit_cards' + autoload :CustomerCreditCardsList, 'netsuite/records/customer_credit_cards_list' autoload :CustomerCurrency, 'netsuite/records/customer_currency' autoload :CustomerCurrencyList, 'netsuite/records/customer_currency_list' autoload :CustomerDeposit, 'netsuite/records/customer_deposit' @@ -156,6 +158,9 @@ module Records autoload :Duration, 'netsuite/records/duration' autoload :Employee, 'netsuite/records/employee' autoload :EntityCustomField, 'netsuite/records/entity_custom_field' + autoload :Estimate, 'netsuite/records/estimate' + autoload :EstimateItem, 'netsuite/records/estimate_item' + autoload :EstimateItemList, 'netsuite/records/estimate_item_list' autoload :File, 'netsuite/records/file' autoload :GiftCertificate, 'netsuite/records/gift_certificate' autoload :GiftCertificateItem, 'netsuite/records/gift_certificate_item' @@ -209,6 +214,7 @@ module Records autoload :LotNumberedInventoryItem, 'netsuite/records/lot_numbered_inventory_item' autoload :MatrixOptionList, 'netsuite/records/matrix_option_list' autoload :MemberList, 'netsuite/records/member_list' + autoload :Message, 'netsuite/records/message' autoload :NonInventorySaleItem, 'netsuite/records/non_inventory_sale_item' autoload :NonInventoryPurchaseItem, 'netsuite/records/non_inventory_purchase_item' autoload :NonInventoryResaleItem, 'netsuite/records/non_inventory_resale_item' @@ -262,6 +268,7 @@ module Records autoload :Subsidiary, 'netsuite/records/subsidiary' autoload :SubtotalItem, 'netsuite/records/subtotal_item' autoload :SupportCase, 'netsuite/records/support_case' + autoload :SupportCaseType, 'netsuite/records/support_case_type' autoload :TaxType, 'netsuite/records/tax_type' autoload :TaxGroup, 'netsuite/records/tax_group' autoload :Task, 'netsuite/records/task' diff --git a/lib/netsuite/actions/update.rb b/lib/netsuite/actions/update.rb index 37e822349..c7e684274 100644 --- a/lib/netsuite/actions/update.rb +++ b/lib/netsuite/actions/update.rb @@ -8,11 +8,18 @@ class Update def initialize(klass, attributes) @klass = klass + @web_services_preferences = {} + + if attributes.has_key?(:run_suite_scripts) + @web_services_preferences[:run_suite_scripts] = attributes.delete(:run_suite_scripts) + end + @attributes = attributes end def request(credentials={}) - NetSuite::Configuration.connection({}, credentials).call :update, :message => request_body + connection_params = { web_services_preferences: @web_services_preferences } + NetSuite::Configuration.connection(connection_params, credentials).call :update, :message => request_body end # @@ -70,19 +77,25 @@ def errors end module Support - def update(options = {}, credentials={}) - options[:internal_id] = internal_id if respond_to?(:internal_id) && internal_id + def update(options = {}, credentials = {}, web_services_preferences = {}) + + if web_services_preferences.has_key?(:run_suite_scripts) + options.merge!(run_suite_scripts: web_services_preferences[:run_suite_scripts]) + end + + options.merge!(:internal_id => internal_id) if respond_to?(:internal_id) && internal_id if !options.include?(:external_id) && (respond_to?(:external_id) && external_id) options[:external_id] = external_id end + options.merge!(:external_id => external_id) if respond_to?(:external_id) && external_id + response = NetSuite::Actions::Update.call([self.class, options], credentials) @errors = response.errors response.success? end end - end end end diff --git a/lib/netsuite/configuration.rb b/lib/netsuite/configuration.rb index 20acf88df..38130750b 100644 --- a/lib/netsuite/configuration.rb +++ b/lib/netsuite/configuration.rb @@ -15,12 +15,14 @@ def attributes end def connection(params={}, credentials={}) + preferences = params.delete(:web_services_preferences) + client = Savon.client({ wsdl: cached_wsdl || wsdl, read_timeout: read_timeout, open_timeout: open_timeout, namespaces: namespaces, - soap_header: auth_header(credentials).update(soap_header), + soap_header: auth_header(credentials).update(soap_header_with_web_preferences_headers(preferences)), pretty_print_xml: true, filters: filters, logger: logger, @@ -350,7 +352,10 @@ def log(path = nil) def logger(value = nil) if value.nil? - attributes[:logger] ||= ::Logger.new((log && !log.empty?) ? log : $stdout) + # if passed a IO object (like StringIO) `empty?` won't exist + valid_log = log && !(log.respond_to?(:empty?) && log.empty?) + + attributes[:logger] ||= ::Logger.new(valid_log ? log : $stdout) else attributes[:logger] = value end @@ -370,12 +375,27 @@ def silent=(value) end def log_level(value = nil) - self.log_level = value || :debug - attributes[:log_level] + self.log_level = value if value + + attributes[:log_level] || :debug end def log_level=(value) - attributes[:log_level] ||= value + attributes[:log_level] = value + end + + def soap_header_with_web_preferences_headers(web_services_preferences) + base_soap_header = soap_header.dup + + return base_soap_header if web_services_preferences.nil? + + if web_services_preferences.has_key?(:run_suite_scripts) + base_soap_header['platformMsgs:preferences'] ||= {} + run_scripts_tag = 'platformMsgs:runServerSuiteScriptAndTriggerWorkflows' + base_soap_header['platformMsgs:preferences'][run_scripts_tag] = web_services_preferences[:run_suite_scripts] + end + + base_soap_header end end end diff --git a/lib/netsuite/records/classification.rb b/lib/netsuite/records/classification.rb index f74fd0b20..745cc2e0b 100644 --- a/lib/netsuite/records/classification.rb +++ b/lib/netsuite/records/classification.rb @@ -9,9 +9,12 @@ class Classification actions :add, :get, :get_list, :delete, :upsert, :search - fields :name, :include_children, :is_inactive, :class_translation_list, :custom_field_list, :parent + fields :name, :include_children, :is_inactive, :class_translation_list field :subsidiary_list, RecordRefList + field :custom_field_list, CustomFieldList + + record_refs :parent attr_reader :internal_id attr_accessor :external_id diff --git a/lib/netsuite/records/custom_record.rb b/lib/netsuite/records/custom_record.rb index d12fa0915..60fd4f1de 100644 --- a/lib/netsuite/records/custom_record.rb +++ b/lib/netsuite/records/custom_record.rb @@ -18,7 +18,7 @@ class CustomRecord field :custom_field_list, CustomFieldList - record_refs :custom_form, :owner, :rec_type + record_refs :custom_form, :owner, :rec_type, :parent attr_reader :internal_id attr_accessor :external_id diff --git a/lib/netsuite/records/customer.rb b/lib/netsuite/records/customer.rb index 2155dc5bd..055088713 100644 --- a/lib/netsuite/records/customer.rb +++ b/lib/netsuite/records/customer.rb @@ -13,7 +13,7 @@ class Customer fields :account_number, :aging, :alt_email, :alt_name, :alt_phone, :bill_pay, :buying_reason, :buying_time_frame, :campaign_category, :click_stream, :comments, :company_name, - :consol_aging, :consol_days_overdue, :contrib_pct, :credit_cards_list, :credit_hold_override, + :consol_aging, :consol_days_overdue, :contrib_pct, :credit_hold_override, :credit_limit, :date_created, :days_overdue, :default_address, :download_list, :email, :email_preference, :email_transactions, :end_date, :entity_id, :estimated_budget, :fax, :fax_transactions, :first_name, :first_visit, :give_access, :global_subscription_status, @@ -28,6 +28,7 @@ class Customer :vat_reg_number, :visits, :web_lead field :addressbook_list, CustomerAddressbookList + field :credit_cards_list, CustomerCreditCardsList field :custom_field_list, CustomFieldList field :contact_roles_list, ContactAccessRolesList field :currency_list, CustomerCurrencyList diff --git a/lib/netsuite/records/customer_credit_cards.rb b/lib/netsuite/records/customer_credit_cards.rb new file mode 100644 index 000000000..caeb1b786 --- /dev/null +++ b/lib/netsuite/records/customer_credit_cards.rb @@ -0,0 +1,36 @@ +module NetSuite + module Records + class CustomerCreditCards + include Support::Fields + include Support::RecordRefs + include Support::Records + + # https://system.netsuite.com/help/helpcenter/en_US/srbrowser/Browser2017_1/schema/other/customercreditcards.html?mode=package + + fields :cc_default, :cc_expire_date, :cc_memo, :cc_name, :cc_number, :debitcard_issue_no, :state_from, :validfrom + record_refs :card_state, :payment_method + + attr_reader :internal_id + + def initialize(attributes_or_record = {}) + case attributes_or_record + when self.class + initialize_from_record(attributes_or_record) + when Hash + initialize_from_attributes_hash(attributes_or_record) + end + end + + def initialize_from_record(obj) + self.cc_default = obj.cc_default + self.cc_expire_date = obj.cc_expire_date + self.cc_memo = obj.cc_memo + self.cc_name = obj.cc_name + self.cc_number = obj.cc_number + self.debitcard_issue_no = obj.debitcard_issue_no + self.state_from = obj.state_from + self.validfrom = obj.validfrom + end + end + end +end diff --git a/lib/netsuite/records/customer_credit_cards_list.rb b/lib/netsuite/records/customer_credit_cards_list.rb new file mode 100644 index 000000000..b2e060dfc --- /dev/null +++ b/lib/netsuite/records/customer_credit_cards_list.rb @@ -0,0 +1,10 @@ +module NetSuite + module Records + class CustomerCreditCardsList < Support::Sublist + include Namespaces::ListRel + + sublist :credit_cards, CustomerCreditCards + alias :credit_card :credit_cards + end + end +end diff --git a/lib/netsuite/records/customer_payment.rb b/lib/netsuite/records/customer_payment.rb index 9baab81fc..e6a4bb141 100644 --- a/lib/netsuite/records/customer_payment.rb +++ b/lib/netsuite/records/customer_payment.rb @@ -25,6 +25,7 @@ class CustomerPayment attr_reader :internal_id attr_accessor :external_id + attr_accessor :search_joins def initialize(attributes = {}) @internal_id = attributes.delete(:internal_id) || attributes.delete(:@internal_id) diff --git a/lib/netsuite/records/employee.rb b/lib/netsuite/records/employee.rb index 5b59cbc71..4df2f03d0 100644 --- a/lib/netsuite/records/employee.rb +++ b/lib/netsuite/records/employee.rb @@ -19,7 +19,7 @@ class Employee :phonetic_name, :purchase_order_approval_limit, :purchase_order_approver, :purchase_order_limit, :release_date, :resident_status, :salutation, :social_security_number, :visa_exp_date, :visa_type - record_refs :currency, :department, :location, :subsidiary, :employee_type, :employee_status, :supervisor + record_refs :currency, :department, :location, :sales_role, :subsidiary, :employee_type, :employee_status, :supervisor field :custom_field_list, CustomFieldList field :roles_list, RoleList diff --git a/lib/netsuite/records/estimate.rb b/lib/netsuite/records/estimate.rb new file mode 100644 index 000000000..cc1602f59 --- /dev/null +++ b/lib/netsuite/records/estimate.rb @@ -0,0 +1,42 @@ +module NetSuite + module Records + class Estimate + include Support::Fields + include Support::RecordRefs + include Support::Records + include Support::Actions + include Namespaces::TranSales + + actions :get, :get_list, :add, :initialize, :delete, :update, :upsert, :search + + fields :alt_handling_cost, :alt_sales_total, :alt_shipping_cost, :balance, + :bill_address, :billing_address, :billing_schedule, :bill_is_residential, + :created_date, :currency_name, :discount_rate, :email, :end_date, + :est_gross_profit, :exchange_rate, :handling_cost, :handling_tax1_rate, :is_taxable, + :last_modified_date, :memo, :message, :other_ref_num, :ship_date, :shipping_cost, + :shipping_tax1_rate, :source, :start_date, :status, :sync_partner_teams, :sync_sales_teams, + :to_be_emailed, :to_be_faxed, :to_be_printed, :total_cost_estimate, :tran_date, :tran_id, + :linked_tracking_numbers, :is_multi_ship_to + + field :shipping_address, Address + field :billing_address, Address + + field :item_list, EstimateItemList + field :custom_field_list, CustomFieldList + + record_refs :bill_address_list, :created_from, :currency, :custom_form, :department, :discount_item, :entity, + :handling_tax_code, :job, :klass, :lead_source, :location, :message_sel, :opportunity, :partner, + :promo_code, :sales_group, :sales_rep, :ship_method, :shipping_tax_code, :subsidiary, :terms + + attr_reader :internal_id + attr_accessor :external_id + attr_accessor :search_joins + + def initialize(attributes = {}) + @internal_id = attributes.delete(:internal_id) || attributes.delete(:@internal_id) + @external_id = attributes.delete(:external_id) || attributes.delete(:@external_id) + initialize_from_attributes_hash(attributes) + end + end + end +end diff --git a/lib/netsuite/records/estimate_item.rb b/lib/netsuite/records/estimate_item.rb new file mode 100644 index 000000000..433725af4 --- /dev/null +++ b/lib/netsuite/records/estimate_item.rb @@ -0,0 +1,40 @@ +module NetSuite + module Records + class EstimateItem + include Support::Fields + include Support::RecordRefs + include Support::Records + include Namespaces::TranSales + + fields :amount, :cost_estimate, + :cost_estimate_type, :defer_rev_rec, :description, + :is_taxable, :line, :quantity, + :rate, :tax_rate1 + + field :custom_field_list, CustomFieldList + + record_refs :item, :job, :price, :tax_code, :units + + def initialize(attributes_or_record = {}) + case attributes_or_record + when Hash + initialize_from_attributes_hash(attributes_or_record) + when self.class + initialize_from_record(attributes_or_record) + end + end + + def initialize_from_record(record) + self.attributes = record.send(:attributes) + end + + def to_record + rec = super + if rec["#{record_namespace}:customFieldList"] + rec["#{record_namespace}:customFieldList!"] = rec.delete("#{record_namespace}:customFieldList") + end + rec + end + end + end +end diff --git a/lib/netsuite/records/estimate_item_list.rb b/lib/netsuite/records/estimate_item_list.rb new file mode 100644 index 000000000..7c191cbff --- /dev/null +++ b/lib/netsuite/records/estimate_item_list.rb @@ -0,0 +1,11 @@ +module NetSuite + module Records + class EstimateItemList < Support::Sublist + include Namespaces::TranSales + + sublist :item, EstimateItem + + alias :items :item + end + end +end diff --git a/lib/netsuite/records/invoice.rb b/lib/netsuite/records/invoice.rb index 99ea37250..60f25aba5 100644 --- a/lib/netsuite/records/invoice.rb +++ b/lib/netsuite/records/invoice.rb @@ -39,7 +39,7 @@ class Invoice field :shipping_address, Address field :billing_address, Address - read_only_fields :sub_total, :discount_total, :total, :recognized_revenue, :amount_remaining, :amount_paid, + read_only_fields :sub_total, :discount_total, :total, :recognized_revenue, :amount_remaining, :amount_paid, :amount, :alt_shipping_cost, :gift_cert_applied, :handling_cost, :alt_handling_cost record_refs :account, :bill_address_list, :custom_form, :department, :entity, :klass, :partner, diff --git a/lib/netsuite/records/item_fulfillment.rb b/lib/netsuite/records/item_fulfillment.rb index fd5e691d3..aa37b4546 100644 --- a/lib/netsuite/records/item_fulfillment.rb +++ b/lib/netsuite/records/item_fulfillment.rb @@ -11,7 +11,7 @@ class ItemFulfillment fields :tran_date, :tran_id, :shipping_cost, :memo, :ship_company, :ship_attention, :ship_addr1, :ship_addr2, :ship_city, :ship_state, :ship_zip, :ship_phone, :ship_is_residential, - :ship_status, :last_modified_date, :created_date + :ship_status, :last_modified_date, :created_date, :status read_only_fields :handling_cost diff --git a/lib/netsuite/records/matrix_option_list.rb b/lib/netsuite/records/matrix_option_list.rb index 1d8fe4403..af0790205 100644 --- a/lib/netsuite/records/matrix_option_list.rb +++ b/lib/netsuite/records/matrix_option_list.rb @@ -15,10 +15,14 @@ class MatrixOptionList # # # - # + # + # foo + # # # - # + # + # bar + # # # # @@ -27,13 +31,17 @@ def initialize(attributes = {}) when Hash options << OpenStruct.new( type_id: attributes[:matrix_option][:value][:'@type_id'], - value_id: attributes[:matrix_option][:value][:'@internal_id'] + value_id: attributes[:matrix_option][:value][:'@internal_id'], + script_id: attributes[:matrix_option][:@script_id], + name: attributes[:matrix_option][:value][:name] ) when Array attributes[:matrix_option].each do |option| options << OpenStruct.new( type_id: option[:value][:'@type_id'], - value_id: option[:value][:'@internal_id'] + value_id: option[:value][:'@internal_id'], + script_id: option[:@script_id], + name: option[:value][:name] ) end end diff --git a/lib/netsuite/records/message.rb b/lib/netsuite/records/message.rb new file mode 100644 index 000000000..e924f9df9 --- /dev/null +++ b/lib/netsuite/records/message.rb @@ -0,0 +1,30 @@ +module NetSuite + module Records + class Message + include Support::Fields + include Support::RecordRefs + include Support::Records + include Support::Actions + include Namespaces::CommGeneral + + actions :get, :add, :delete, :search + + fields :bcc, :cc, :compress_attachments, :date_time, :emailed, :incoming, + :message, :record_name, :record_type_name, :subject + + read_only_fields :last_modified_date, :message_date + + record_refs :activity, :author, :recipient, :transaction + + attr_reader :internal_id + attr_accessor :external_id + + def initialize(attributes_or_record = {}) + @internal_id = attributes_or_record.delete(:internal_id) || attributes_or_record.delete(:@internal_id) + @external_id = attributes_or_record.delete(:external_id) || attributes_or_record.delete(:@external_id) + initialize_from_attributes_hash(attributes_or_record) + end + + end + end +end diff --git a/lib/netsuite/records/non_inventory_resale_item.rb b/lib/netsuite/records/non_inventory_resale_item.rb index 0628c8c6a..832dda4f0 100644 --- a/lib/netsuite/records/non_inventory_resale_item.rb +++ b/lib/netsuite/records/non_inventory_resale_item.rb @@ -33,6 +33,7 @@ class NonInventoryResaleItem field :custom_field_list, CustomFieldList field :pricing_matrix, PricingMatrix field :subsidiary_list, RecordRefList + field :item_vendor_list, ItemVendorList attr_reader :internal_id diff --git a/lib/netsuite/records/non_inventory_sale_item.rb b/lib/netsuite/records/non_inventory_sale_item.rb index 75d49770b..7699b4637 100644 --- a/lib/netsuite/records/non_inventory_sale_item.rb +++ b/lib/netsuite/records/non_inventory_sale_item.rb @@ -10,7 +10,7 @@ class NonInventorySaleItem actions :get, :get_list, :add, :delete, :search, :update, :upsert fields :available_to_partners, :cost_estimate, :cost_estimate_type, :cost_estimate_units, :country_of_manufacture, - :created_date, :display_name, :dont_show_price, :enforce_min_qty_internally, :exclude_from_sitemap, + :created_date, :direct_revenue_posting, :display_name, :dont_show_price, :enforce_min_qty_internally, :exclude_from_sitemap, :featured_description, :handling_cost, :handling_cost_units, :include_children, :is_donation_item, :is_fulfillable, :is_gco_compliant, :is_inactive, :is_online, :is_taxable, :item_id, :last_modified_date, :manufacturer, :manufacturer_addr1, :manufacturer_city, :manufacturer_state, :manufacturer_tariff, :manufacturer_tax_id, diff --git a/lib/netsuite/records/other_charge_sale_item.rb b/lib/netsuite/records/other_charge_sale_item.rb index cbb649e1f..874643327 100644 --- a/lib/netsuite/records/other_charge_sale_item.rb +++ b/lib/netsuite/records/other_charge_sale_item.rb @@ -52,11 +52,11 @@ class OtherChargeSaleItem :units_type, :sales_tax_code, :sale_unit, :tax_schedule, :parent field :custom_field_list, CustomFieldList - # :pricing_matrix, + field :pricing_matrix, PricingMatrix # :translations_list, # :matrix_option_list, # :item_options_list - # :subsidiary_list, + field :subsidiary_list, RecordRefList def initialize(attributes = {}) @internal_id = attributes.delete(:internal_id) || attributes.delete(:@internal_id) diff --git a/lib/netsuite/records/partner.rb b/lib/netsuite/records/partner.rb index 04d0b93eb..58232e3a9 100644 --- a/lib/netsuite/records/partner.rb +++ b/lib/netsuite/records/partner.rb @@ -16,7 +16,7 @@ class Partner :partner_code, :is_person, :company_name, :eligible_for_commission, :entity_id, :last_modified_date, :date_created, :title, :mobile_phone, :comments, :middle_name, :send_email, :password, :password2 - record_refs :klass, :access_role, :department + record_refs :klass, :access_role, :department, :subsidiary attr_reader :internal_id attr_accessor :external_id diff --git a/lib/netsuite/records/service_resale_item.rb b/lib/netsuite/records/service_resale_item.rb index fc04cc5a6..d5ee833b5 100644 --- a/lib/netsuite/records/service_resale_item.rb +++ b/lib/netsuite/records/service_resale_item.rb @@ -9,7 +9,7 @@ class ServiceResaleItem actions :get, :get_list, :add, :update, :delete, :upsert, :search - fields :available_to_partners, :cost_estimate, :cost_estimate_type, :cost_estimate_units, :create_job, :created_date, + fields :available_to_partners, :cost, :cost_estimate, :cost_estimate_type, :cost_estimate_units, :create_job, :created_date, :display_name, :dont_show_price, :enforce_min_qty_internally, :exclude_from_sitemap, :featured_description, :include_children, :is_donation_item, :is_fulfillable, :is_gco_compliant, :is_inactive, :is_online, :is_taxable, :item_id, :last_modified_date, :matrix_option_list, :matrix_type, :max_donation_amount, :meta_tag_html, diff --git a/lib/netsuite/records/support_case_type.rb b/lib/netsuite/records/support_case_type.rb new file mode 100644 index 000000000..63152d3ce --- /dev/null +++ b/lib/netsuite/records/support_case_type.rb @@ -0,0 +1,26 @@ +module NetSuite + module Records + class SupportCaseType + include Support::Fields + include Support::RecordRefs + include Support::Records + include Support::Actions + include Namespaces::ListSupport + + actions :get + + fields :description, :is_inactive, :name + + record_refs :insert_before + + attr_reader :internal_id + attr_accessor :external_id + + def initialize(attributes = {}) + @internal_id = attributes.delete(:internal_id) || attributes.delete(:@internal_id) + @external_id = attributes.delete(:external_id) || attributes.delete(:@external_id) + initialize_from_attributes_hash(attributes) + end + end + end +end diff --git a/lib/netsuite/support/search_result.rb b/lib/netsuite/support/search_result.rb index ddf1f2938..901c46edf 100644 --- a/lib/netsuite/support/search_result.rb +++ b/lib/netsuite/support/search_result.rb @@ -29,8 +29,19 @@ def initialize(response, result_class, credentials) if @total_records > 0 if response.body.has_key?(:record_list) # basic search results - record_list = response.body[:record_list][:record] - record_list = [record_list] unless record_list.is_a?(Array) + + # `recordList` node can contain several nested `record` nodes, only one node or be empty + # so we have to handle all these cases: + # * { record_list: nil } + # * { record_list: { record: => {...} } } + # * { record_list: { record: => [{...}, {...}, ...] } } + record_list = if response.body[:record_list].nil? + [] + elsif response.body[:record_list][:record].is_a?(Array) + response.body[:record_list][:record] + else + [response.body[:record_list][:record]] + end record_list.each do |record| results << result_class.new(record) diff --git a/lib/netsuite/utilities.rb b/lib/netsuite/utilities.rb index 1b5c0bc03..b0813a3ea 100644 --- a/lib/netsuite/utilities.rb +++ b/lib/netsuite/utilities.rb @@ -78,7 +78,7 @@ def backoff(options = {}) begin count += 1 yield - rescue Exception => e + rescue StandardError => e exceptions_to_retry = [ Errno::ECONNRESET, Errno::ETIMEDOUT, @@ -103,6 +103,10 @@ def backoff(options = {}) exceptions_to_retry << OpenSSL::SSL::SSLErrorWaitReadable if defined?(OpenSSL::SSL::SSLErrorWaitReadable) # depends on the http library chosen + exceptions_to_retry << HTTPClient::TimeoutError if defined?(HTTPClient::TimeoutError) + exceptions_to_retry << HTTPClient::ConnectTimeoutError if defined?(HTTPClient::ConnectTimeoutError) + exceptions_to_retry << HTTPClient::ReceiveTimeoutError if defined?(HTTPClient::ReceiveTimeoutError) + exceptions_to_retry << HTTPClient::SendTimeoutError if defined?(HTTPClient::SendTimeoutError) exceptions_to_retry << Excon::Error::Timeout if defined?(Excon::Error::Timeout) exceptions_to_retry << Excon::Error::Socket if defined?(Excon::Error::Socket) @@ -115,6 +119,7 @@ def backoff(options = {}) # https://github.com/stripe/stripe-netsuite/issues/815 if !e.message.include?("Only one request may be made against a session at a time") && !e.message.include?('java.util.ConcurrentModificationException') && + !e.message.include?('java.lang.NullPointerException') && !e.message.include?('java.lang.IllegalStateException') && !e.message.include?('java.lang.reflect.InvocationTargetException') && !e.message.include?('com.netledger.common.exceptions.NLDatabaseOfflineException') && @@ -173,6 +178,7 @@ def get_item(ns_item_internal_id, opts = {}) ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::KitItem, ns_item_internal_id, opts) ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::SerializedInventoryItem, ns_item_internal_id, opts) ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::LotNumberedAssemblyItem, ns_item_internal_id, opts) + ns_item ||= NetSuite::Utilities.get_record(NetSuite::Records::LotNumberedInventoryItem, ns_item_internal_id, opts) if ns_item.nil? fail NetSuite::RecordNotFound, "item with ID #{ns_item_internal_id} not found" diff --git a/lib/netsuite/version.rb b/lib/netsuite/version.rb index 23293d429..438623e3c 100644 --- a/lib/netsuite/version.rb +++ b/lib/netsuite/version.rb @@ -1,3 +1,3 @@ module NetSuite - VERSION = '0.8.4' + VERSION = '0.8.6' end diff --git a/netsuite.gemspec b/netsuite.gemspec index ef89fd4e5..fb4ca5452 100644 --- a/netsuite.gemspec +++ b/netsuite.gemspec @@ -2,8 +2,9 @@ require File.expand_path('../lib/netsuite/version', __FILE__) Gem::Specification.new do |gem| + gem.licenses = ['MIT'] gem.authors = ['Ryan Moran', 'Michael Bianco'] - gem.email = ['ryan.moran@gmail.com', 'mike@cliffsidemedia.com'] + gem.email = ['ryan.moran@gmail.com', 'mike@mikebian.co'] gem.description = %q{NetSuite SuiteTalk API Wrapper} gem.summary = %q{NetSuite SuiteTalk API (SOAP) Wrapper} gem.homepage = 'https://github.com/NetSweet/netsuite' @@ -15,7 +16,7 @@ Gem::Specification.new do |gem| gem.require_paths = ['lib'] gem.version = NetSuite::VERSION - gem.add_dependency 'savon', '>= 2.3.0' + gem.add_dependency 'savon', '>= 2.3.0', '<= 2.11.1' gem.add_development_dependency 'rspec', '~> 3.8.0' end diff --git a/spec/netsuite/configuration_spec.rb b/spec/netsuite/configuration_spec.rb index 53cd6b753..7b48a0c6f 100644 --- a/spec/netsuite/configuration_spec.rb +++ b/spec/netsuite/configuration_spec.rb @@ -326,15 +326,11 @@ describe "#credentials" do context "when none are defined" do - skip "should properly create the auth credentials" do - - end + skip "should properly create the auth credentials" end context "when they are defined" do - it "should properly replace the default auth credentials" do - - end + skip "should properly replace the default auth credentials" end end @@ -371,6 +367,65 @@ end end + describe "#log" do + it 'allows a file path to be set as the log destination' do + file_path = Tempfile.new.path + config.log = file_path + config.logger.info "foo" + + log_contents = open(file_path).read + expect(log_contents).to include("foo") + end + + it 'allows an IO device to bet set as the log destination' do + stream = StringIO.new + config.log = stream + config.logger.info "foo" + + expect(stream.string).to include("foo") + end + end + + describe '#log_level' do + it 'defaults to :debug' do + expect(config.log_level).to eq(:debug) + end + + it 'can be initially set to any log level' do + config.log_level(:info) + + expect(config.log_level).to eq(:info) + end + + it 'can override itself' do + config.log_level = :info + + expect(config.log_level).to eq(:info) + + config.log_level(:debug) + + expect(config.log_level).to eq(:debug) + end + end + + describe '#log_level=' do + it 'can set the initial log_level' do + config.log_level = :info + + expect(config.log_level).to eq(:info) + end + + it 'can override a previously set log level' do + config.log_level = :info + + expect(config.log_level).to eq(:info) + + config.log_level = :debug + + expect(config.log_level).to eq(:debug) + end + end + describe 'timeouts' do it 'has defaults' do expect(config.read_timeout).to eql(60) diff --git a/spec/netsuite/records/classification_spec.rb b/spec/netsuite/records/classification_spec.rb index 8ee89f301..f193d3480 100644 --- a/spec/netsuite/records/classification_spec.rb +++ b/spec/netsuite/records/classification_spec.rb @@ -5,12 +5,21 @@ it 'has all the right fields' do [ - :name, :include_children, :is_inactive, :class_translation_list, :custom_field_list + :name, :include_children, :is_inactive, :class_translation_list ].each do |field| expect(classification).to have_field(field) end expect(classification.subsidiary_list.class).to eq(NetSuite::Records::RecordRefList) + expect(classification.custom_field_list.class).to eq(NetSuite::Records::CustomFieldList) + end + + it 'has all the right record refs' do + [ + :parent + ].each do |record_ref| + expect(classification).to have_record_ref(record_ref) + end end describe '.get' do diff --git a/spec/netsuite/records/custom_field_list_spec.rb b/spec/netsuite/records/custom_field_list_spec.rb index ea8316c77..f483e30f8 100644 --- a/spec/netsuite/records/custom_field_list_spec.rb +++ b/spec/netsuite/records/custom_field_list_spec.rb @@ -37,9 +37,11 @@ context 'writing convience methods' do it "should create a custom field entry when none exists" do list.custrecord_somefield = 'a value' - list.custom_fields.size.should == 1 - list.custom_fields.first.value.should == 'a value' - list.custom_fields.first.type.should == 'platformCore:StringCustomFieldRef' + custom_fields = list.custom_fields + + expect(custom_fields.size).to eq(1) + expect(custom_fields.first.value).to eq('a value') + expect(custom_fields.first.type).to eq('platformCore:StringCustomFieldRef') end # https://github.com/NetSweet/netsuite/issues/325 @@ -116,7 +118,7 @@ end it "should raise an error if custom field entry does not exist" do - expect{ list.nonexisting_custom_field }.to raise_error + expect{ list.nonexisting_custom_field }.to raise_error(NoMethodError) end end end diff --git a/spec/netsuite/records/custom_record_spec.rb b/spec/netsuite/records/custom_record_spec.rb index bfaf6732b..a3b8994ef 100644 --- a/spec/netsuite/records/custom_record_spec.rb +++ b/spec/netsuite/records/custom_record_spec.rb @@ -18,7 +18,7 @@ it 'has all the right record_refs' do [ - :custom_form, :owner + :custom_form, :owner, :rec_type, :parent ].each do |record_ref| expect(record).to have_record_ref(record_ref) end diff --git a/spec/netsuite/records/customer_credit_cards_list_spec.rb b/spec/netsuite/records/customer_credit_cards_list_spec.rb new file mode 100644 index 000000000..91729274f --- /dev/null +++ b/spec/netsuite/records/customer_credit_cards_list_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe NetSuite::Records::CustomerCreditCardsList do + let(:list) { NetSuite::Records::CustomerCreditCardsList.new } + + it 'has a credit_cards attribute' do + expect(list.credit_cards).to be_kind_of(Array) + end + + describe '#to_record' do + it 'can represent itself as a SOAP record' do + list.replace_all = true + + record = { + 'listRel:creditCards' => [], + 'listRel:replaceAll' => true + } + + expect(list.to_record).to eql(record) + end + end + +end diff --git a/spec/netsuite/records/customer_spec.rb b/spec/netsuite/records/customer_spec.rb index 8e1eec751..37c1add94 100644 --- a/spec/netsuite/records/customer_spec.rb +++ b/spec/netsuite/records/customer_spec.rb @@ -8,7 +8,7 @@ :account_number, :aging, :alt_email, :alt_name, :alt_phone, :balance, :bill_pay, :buying_reason, :buying_time_frame, :campaign_category, :click_stream, :comments, :company_name, :consol_aging, :consol_balance, :consol_days_overdue, :consol_deposit_balance, :consol_overdue_balance, - :consol_unbilled_orders, :contrib_pct, :credit_cards_list, :credit_hold_override, :credit_limit, + :consol_unbilled_orders, :contrib_pct, :credit_hold_override, :credit_limit, :date_created, :days_overdue, :default_address, :deposit_balance, :download_list, :email, :email_preference, :email_transactions, :end_date, :entity_id, :estimated_budget, :fax, :fax_transactions, :first_name, :first_visit, :give_access, :global_subscription_status, @@ -65,6 +65,27 @@ end end + describe '#credit_cards_list' do + it 'can be set from attributes' do + customer.credit_cards_list = { + :credit_cards => { + :internal_id => '1234567', + :cc_default => true, + :cc_expire_date => '2099-12-01T00:00:00.000-08:00' + } + } + + expect(customer.credit_cards_list).to be_kind_of(NetSuite::Records::CustomerCreditCardsList) + expect(customer.credit_cards_list.credit_cards.length).to eql(1) + end + + it 'can be set from a CustomerCreditCardsList object' do + customer_credit_cards_list = NetSuite::Records::CustomerCreditCardsList.new + customer.credit_cards_list = customer_credit_cards_list + expect(customer.credit_cards_list).to eql(customer_credit_cards_list) + end + end + describe '#custom_field_list' do it 'can be set from attributes' do attributes = { diff --git a/spec/netsuite/records/employee_spec.rb b/spec/netsuite/records/employee_spec.rb index 861fe5d2f..ffb9c9ca0 100644 --- a/spec/netsuite/records/employee_spec.rb +++ b/spec/netsuite/records/employee_spec.rb @@ -21,7 +21,7 @@ it 'has all the right record refs' do [ - :location, :employee_status, :employee_type + :currency, :department, :location, :sales_role, :subsidiary, :employee_type, :employee_status, :supervisor ].each do |record_ref| expect(employee).to have_record_ref(record_ref) end @@ -144,7 +144,7 @@ it 'has the right record_refs' do [ - :currency, :department, :location, :subsidiary + :currency, :department, :location, :sales_role, :subsidiary, :employee_type, :employee_status, :supervisor ].each do |record_ref| expect(employee).to have_record_ref(record_ref) end diff --git a/spec/netsuite/records/estimate_item_list_spec.rb b/spec/netsuite/records/estimate_item_list_spec.rb new file mode 100644 index 000000000..094f076c2 --- /dev/null +++ b/spec/netsuite/records/estimate_item_list_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe NetSuite::Records::EstimateItemList do + let(:list) { NetSuite::Records::EstimateItemList.new } + + it 'has a items attribute' do + expect(list.items).to be_kind_of(Array) + end + + describe '#to_record' do + before do + list.items << NetSuite::Records::EstimateItem.new( + :rate => 10 + ) + end + + it 'can represent itself as a SOAP record' do + record = { + 'tranSales:item' => [{ + 'tranSales:rate' => 10 + }] + } + expect(list.to_record).to eql(record) + end + end +end diff --git a/spec/netsuite/records/estimate_item_spec.rb b/spec/netsuite/records/estimate_item_spec.rb new file mode 100644 index 000000000..dd6bc6bd7 --- /dev/null +++ b/spec/netsuite/records/estimate_item_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe NetSuite::Records::EstimateItem do + let(:item) { NetSuite::Records::EstimateItem.new } + + it 'has all the right fields' do + [ + :amount, :cost_estimate, :cost_estimate_type, + :defer_rev_rec, :description, + :is_taxable, :line, :quantity, + :rate, :tax_rate1 + ].each do |field| + expect(item).to have_field(field) + end + end + + it 'has all the right record refs' do + [ + :item, :job, :price, :tax_code, :units + ].each do |record_ref| + expect(item).to have_record_ref(record_ref) + end + end + + describe '#options' do + it 'can be set from attributes' + it 'can be set from a CustomFieldList object' + end + + describe '#inventory_detail' do + it 'can be set from attributes' + it 'can be set from an InventoryDetail object' + end + + describe '#custom_field_list' do + it 'can be set from attributes' + it 'can be set from a CustomFieldList object' + end + +end diff --git a/spec/netsuite/records/estimate_spec.rb b/spec/netsuite/records/estimate_spec.rb new file mode 100644 index 000000000..9c6546696 --- /dev/null +++ b/spec/netsuite/records/estimate_spec.rb @@ -0,0 +1,216 @@ +require 'spec_helper' + +describe NetSuite::Records::Estimate do + let(:estimate) { NetSuite::Records::Estimate.new } + let(:customer) { NetSuite::Records::Customer.new } + let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) } + + it 'has all the right fields' do + [ + :alt_handling_cost, :alt_shipping_cost, + :balance, :bill_address, :created_date, :currency_name, + :discount_rate, :email, :end_date, :est_gross_profit, + :exchange_rate, + :handling_cost, :handling_tax1_rate, :is_taxable, + :last_modified_date, :memo, :message, :other_ref_num, + :shipping_cost, :shipping_tax1_rate, + :source, :start_date, :status, :sync_partner_teams, + :sync_sales_teams, :to_be_emailed, :to_be_faxed, :to_be_printed, + :total_cost_estimate, :tran_date, :tran_id + ].each do |field| + expect(estimate).to have_field(field) + end + end + + it 'has all the right record refs' do + [ + :bill_address_list, :created_from, :currency, :custom_form, :department, :discount_item, + :entity, :handling_tax_code, :job, :klass, :lead_source, :location, :message_sel, + :opportunity, :partner, :promo_code, :sales_group, :sales_rep, + :ship_method, :shipping_tax_code, :subsidiary + ].each do |record_ref| + expect(estimate).to have_record_ref(record_ref) + end + end + + describe '#order_status' do + it 'can be set from attributes' + it 'can be set from a EstimateStatus object' + end + + describe '#item_list' do + it 'can be set from attributes' do + attributes = { + :item => { + :amount => 10 + } + } + estimate.item_list = attributes + expect(estimate.item_list).to be_kind_of(NetSuite::Records::EstimateItemList) + expect(estimate.item_list.items.length).to eql(1) + end + + it 'can be set from a EstimateItemList object' do + item_list = NetSuite::Records::EstimateItemList.new + estimate.item_list = item_list + expect(estimate.item_list).to eql(item_list) + end + end + + describe '#transaction_bill_address' do + it 'can be set from attributes' + it 'can be set from a BillAddress object' + end + + describe '#transaction_ship_address' do + it 'can be set from attributes' + it 'can be set from a ShipAddress object' + end + + describe '#revenue_status' do + it 'can be set from attributes' + it 'can be set from a RevenueStatus object' + end + + describe '#sales_team_list' do + it 'can be set from attributes' + it 'can be set from a EstimateSalesTeamList object' + end + + describe '#partners_list' do + it 'can be set from attributes' + it 'can be set from a EstimatePartnersList object' + end + + describe '#custom_field_list' do + it 'can be set from attributes' + it 'can be set from a CustomFieldList object' + end + + describe '.get' do + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :alt_shipping_cost => 100 }) } + + it 'returns a Estimate instance populated with the data from the response object' do + expect(NetSuite::Actions::Get).to receive(:call).with([NetSuite::Records::Estimate, :external_id => 1], {}).and_return(response) + estimate = NetSuite::Records::Estimate.get(:external_id => 1) + expect(estimate).to be_kind_of(NetSuite::Records::Estimate) + expect(estimate.alt_shipping_cost).to eql(100) + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'raises a RecordNotFound exception' do + expect(NetSuite::Actions::Get).to receive(:call).with([NetSuite::Records::Estimate, :external_id => 1], {}).and_return(response) + expect { + NetSuite::Records::Estimate.get(:external_id => 1) + }.to raise_error(NetSuite::RecordNotFound, + /NetSuite::Records::Estimate with OPTIONS=(.*) could not be found/) + end + end + end + + describe '.initialize' do + context 'when the request is successful' do + it 'returns an initialized sales order from the customer entity' do + expect(NetSuite::Actions::Initialize).to receive(:call).with([NetSuite::Records::Estimate, customer], {}).and_return(response) + estimate = NetSuite::Records::Estimate.initialize(customer) + expect(estimate).to be_kind_of(NetSuite::Records::Estimate) + end + end + + context 'when the response is unsuccessful' do + skip + end + end + + describe '#add' do + let(:test_data) { { :email => 'test@example.com', :fax => '1234567890' } } + + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) } + + it 'returns true' do + estimate = NetSuite::Records::Estimate.new(test_data) + expect(NetSuite::Actions::Add).to receive(:call). + with([estimate], {}). + and_return(response) + expect(estimate.add).to be_truthy + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'returns false' do + estimate = NetSuite::Records::Estimate.new(test_data) + expect(NetSuite::Actions::Add).to receive(:call). + with([estimate], {}). + and_return(response) + expect(estimate.add).to be_falsey + end + end + end + + describe '#delete' do + let(:test_data) { { :internal_id => '1' } } + + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) } + + it 'returns true' do + estimate = NetSuite::Records::Estimate.new(test_data) + expect(NetSuite::Actions::Delete).to receive(:call). + with([estimate], {}). + and_return(response) + expect(estimate.delete).to be_truthy + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'returns false' do + estimate = NetSuite::Records::Estimate.new(test_data) + expect(NetSuite::Actions::Delete).to receive(:call). + with([estimate], {}). + and_return(response) + expect(estimate.delete).to be_falsey + end + end + end + + describe '#to_record' do + before do + estimate.email = 'something@example.com' + estimate.tran_id = '4' + end + it 'can represent itself as a SOAP record' do + record = { + 'tranSales:email' => 'something@example.com', + 'tranSales:tranId' => '4' + } + expect(estimate.to_record).to eql(record) + end + end + + describe '#record_type' do + it 'returns a string representation of the SOAP type' do + expect(estimate.record_type).to eql('tranSales:Estimate') + end + end + + skip "closing a sales order" do + it "closes each line to close the sales order" do + attributes = sales_order.attributes + attributes[:item_list].items.each do |item| + item.is_closed = true + item.attributes = item.attributes.slice(:line, :is_closed) + end + + sales_order.update({ item_list: attributes[:item_list] }) + end + end +end diff --git a/spec/netsuite/records/matrix_option_list_spec.rb b/spec/netsuite/records/matrix_option_list_spec.rb index 174b74499..f0ccc1cc3 100644 --- a/spec/netsuite/records/matrix_option_list_spec.rb +++ b/spec/netsuite/records/matrix_option_list_spec.rb @@ -5,20 +5,30 @@ module NetSuite module Records describe MatrixOptionList do it "deals with hash properly" do - hash = {:value=>{:@internal_id=>"1", :@type_id=>"36"}} + hash = {:value=>{:@internal_id=>"1", :@type_id=>"36", :name=>"some value"}, :@script_id=>'cust_field_1'} list = described_class.new({ matrix_option: hash }) - expect(list.options.first.value_id).to eq "1" + option = list.options.first + expect(option.value_id).to eq "1" + expect(option.type_id).to eq "36" + expect(option.name).to eq "some value" + expect(option.script_id).to eq "cust_field_1" end it "deals with arrays properly" do array = [ - {:value=>{:@internal_id=>"2", :@type_id=>"28"}}, - {:value=>{:@internal_id=>"1", :@type_id=>"29"}} + {:value=>{:@internal_id=>"2", :@type_id=>"28", :name=>"some value 28"}, :@script_id=>'cust_field_28'}, + {:value=>{:@internal_id=>"1", :@type_id=>"29", :name=>"some value 29"}, :@script_id=>'cust_field_29'} ] list = described_class.new({ matrix_option: array }) - expect(list.options.first.value_id).to eq "2" + expect(list.options.count).to eq 2 + + option = list.options.first + expect(option.value_id).to eq "2" + expect(option.type_id).to eq "28" + expect(option.name).to eq "some value 28" + expect(option.script_id).to eq "cust_field_28" end end end diff --git a/spec/netsuite/records/message_spec.rb b/spec/netsuite/records/message_spec.rb new file mode 100644 index 000000000..7093db7fd --- /dev/null +++ b/spec/netsuite/records/message_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe NetSuite::Records::Message do + let(:message) { NetSuite::Records::Message.new } + + it 'has all the right fields' do + [ + :bcc, :cc, :compress_attachments, :date_time, :emailed, + :incoming, :message, :record_name, :record_type_name, :subject, + :last_modified_date, :message_date + ].each do |field| + expect(message).to have_field(field) + end + end + + it 'has the right record_refs' do + [ + :activity, :author, :recipient, :transaction + ].each do |record_ref| + expect(message).to have_record_ref(record_ref) + end + end + + describe '.get' do + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => {message: 'message text'})} + + it 'returns a Message instance populated with data from the response object' do + expect(NetSuite::Actions::Get).to receive(:call).with([NetSuite::Records::Message, :external_id => 1], {}).and_return(response) + + message = NetSuite::Records::Message.get(:external_id => 1) + expect(message).to be_kind_of(NetSuite::Records::Message) + expect(message.message).to eq('message text') + end + end + + context 'when response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + it 'raises a RecordNotFound exception' do + expect(NetSuite::Actions::Get).to receive(:call).with([NetSuite::Records::Message, :external_id => 1], {}).and_return(response) + expect { + NetSuite::Records::Message.get(:external_id => 1) + }.to raise_error(NetSuite::RecordNotFound, + /NetSuite::Records::Message with OPTIONS=(.*) could not be found/) + end + end + end + +end diff --git a/spec/netsuite/records/non_inventory_resale_item_spec.rb b/spec/netsuite/records/non_inventory_resale_item_spec.rb new file mode 100644 index 000000000..2edd5d143 --- /dev/null +++ b/spec/netsuite/records/non_inventory_resale_item_spec.rb @@ -0,0 +1,165 @@ +require 'spec_helper' + +describe NetSuite::Records::NonInventoryResaleItem do + let(:item) { NetSuite::Records::NonInventoryResaleItem.new } + + it 'has the right fields' do + [ + :available_to_partners, :cost_estimate, :cost_estimate_type, :cost_estimate_units, :country_of_manufacture, :created_date, + :display_name, :dont_show_price, :enforce_min_qty_internally, :exclude_from_sitemap, + :featured_description, :handling_cost, :handling_cost_units, :include_children, :is_donation_item, :is_fulfillable, + :is_gco_compliant, :is_inactive, :is_online, :is_taxable, :item_id, :last_modified_date, :manufacturer, :manufacturer_addr1, + :manufacturer_city, :manufacturer_state, :manufacturer_tariff, :manufacturer_tax_id, :manufacturer_zip, :matrix_option_list, + :matrix_type, :max_donation_amount, :meta_tag_html, :minimum_quantity, :minimum_quantity_units, :mpn, + :mult_manufacture_addr, :nex_tag_category, :no_price_message, :offer_support, :on_special, :out_of_stock_behavior, + :out_of_stock_message, :overall_quantity_pricing_type, :page_title, :preference_criterion, :presentation_item_list, + :prices_include_tax, :producer, :product_feed_list, :rate, :related_items_description, :sales_description, + :schedule_b_code, :schedule_b_number, :schedule_b_quantity, :search_keywords, :ship_individually, :shipping_cost, + :shipping_cost_units, :shopping_dot_com_category, :shopzilla_category_id, :show_default_donation_amount, + :site_category_list, :sitemap_priority, :soft_descriptor, :specials_description, :stock_description, :store_description, + :store_detailed_description, :store_display_name, :translations_list, :upc_code, :url_component, :use_marginal_rates, + :vsoe_deferral, :vsoe_delivered, :vsoe_permit_discount, :vsoe_price, :weight, :weight_unit, :weight_units + ].each do |field| + expect(item).to have_field(field) + end + + # TODO there is a probably a more robust way to test this + expect(item.custom_field_list.class).to eq(NetSuite::Records::CustomFieldList) + expect(item.pricing_matrix.class).to eq(NetSuite::Records::PricingMatrix) + expect(item.subsidiary_list.class).to eq(NetSuite::Records::RecordRefList) + expect(item.item_vendor_list.class).to eq(NetSuite::Records::ItemVendorList) + + end + + it 'has the right record_refs' do + [ + :billing_schedule, :cost_category, :custom_form, :deferred_revenue_account, :department, :income_account, + :issue_product, :item_options_list, :klass, :location, :parent, :pricing_group, :purchase_tax_code, + :quantity_pricing_schedule, :rev_rec_schedule, :sale_unit, :sales_tax_code, :ship_package, :store_display_image, + :store_display_thumbnail, :store_item_template, :tax_schedule, :units_type, :expense_account + ].each do |record_ref| + expect(item).to have_record_ref(record_ref) + end + end + + describe '.get' do + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :manufacturer_zip => '90401' }) } + + it 'returns a NonInventoryResaleItem instance populated with the data from the response object' do + expect(NetSuite::Actions::Get).to receive(:call).with([NetSuite::Records::NonInventoryResaleItem, {:external_id => 20}], {}).and_return(response) + customer = NetSuite::Records::NonInventoryResaleItem.get(:external_id => 20) + expect(customer).to be_kind_of(NetSuite::Records::NonInventoryResaleItem) + expect(customer.manufacturer_zip).to eql('90401') + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'raises a RecordNotFound exception' do + expect(NetSuite::Actions::Get).to receive(:call).with([NetSuite::Records::NonInventoryResaleItem, {:external_id => 20}], {}).and_return(response) + expect { + NetSuite::Records::NonInventoryResaleItem.get(:external_id => 20) + }.to raise_error(NetSuite::RecordNotFound, + /NetSuite::Records::NonInventoryResaleItem with OPTIONS=(.*) could not be found/) + end + end + end + + describe '#add' do + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) } + + it 'returns true' do + expect(NetSuite::Actions::Add).to receive(:call). + with([item], {}). + and_return(response) + expect(item.add).to be_truthy + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'returns false' do + expect(NetSuite::Actions::Add).to receive(:call). + with([item], {}). + and_return(response) + expect(item.add).to be_falsey + end + end + end + + describe '#update' do + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) } + + it 'returns true' do + expect(NetSuite::Actions::Update).to receive(:call). + with([item.class, {external_id: 'foo'}], {}). + and_return(response) + item.external_id = 'foo' + expect(item.update).to be_truthy + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'returns false' do + expect(NetSuite::Actions::Update).to receive(:call). + with([item.class, {external_id: 'foo'}], {}). + and_return(response) + item.external_id = 'foo' + expect(item.update).to be_falsey + end + end + end + + describe '#delete' do + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) } + + it 'returns true' do + expect(NetSuite::Actions::Delete).to receive(:call). + with([item], {}). + and_return(response) + expect(item.delete).to be_truthy + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'returns false' do + expect(NetSuite::Actions::Delete).to receive(:call). + with([item], {}). + and_return(response) + expect(item.delete).to be_falsey + end + end + end + + describe '#to_record' do + before do + item.handling_cost = 100.0 + item.is_online = true + end + + it 'can represent itself as a SOAP record' do + record = { + 'listAcct:handlingCost' => 100.0, + 'listAcct:isOnline' => true + } + expect(item.to_record).to eql(record) + end + end + + describe '#record_type' do + it 'returns a string of the SOAP type' do + expect(item.record_type).to eql('listAcct:NonInventoryResaleItem') + end + end + +end diff --git a/spec/netsuite/records/non_inventory_sale_item_spec.rb b/spec/netsuite/records/non_inventory_sale_item_spec.rb index 6e5902233..723324f39 100644 --- a/spec/netsuite/records/non_inventory_sale_item_spec.rb +++ b/spec/netsuite/records/non_inventory_sale_item_spec.rb @@ -6,7 +6,7 @@ it 'has the right fields' do [ :available_to_partners, :cost_estimate, :cost_estimate_type, :cost_estimate_units, :country_of_manufacture, :created_date, - :display_name, :dont_show_price, :enforce_min_qty_internally, :exclude_from_sitemap, + :direct_revenue_posting, :display_name, :dont_show_price, :enforce_min_qty_internally, :exclude_from_sitemap, :featured_description, :handling_cost, :handling_cost_units, :include_children, :is_donation_item, :is_fulfillable, :is_gco_compliant, :is_inactive, :is_online, :is_taxable, :item_id, :last_modified_date, :manufacturer, :manufacturer_addr1, :manufacturer_city, :manufacturer_state, :manufacturer_tariff, :manufacturer_tax_id, :manufacturer_zip, :matrix_option_list, diff --git a/spec/netsuite/records/partner_spec.rb b/spec/netsuite/records/partner_spec.rb new file mode 100644 index 000000000..197a2ef54 --- /dev/null +++ b/spec/netsuite/records/partner_spec.rb @@ -0,0 +1,141 @@ +require 'spec_helper' + +describe NetSuite::Records::Partner do + let(:partner) { NetSuite::Records::Partner.new } + + it 'has all the right fields' do + [ + :phone, :home_phone, :first_name, :last_name, :alt_name, :is_inactive, :email, :give_access, + :partner_code, :is_person, :company_name, :eligible_for_commission, :entity_id, :last_modified_date, + :date_created, :title, :mobile_phone, :comments, :middle_name, :send_email, :password, :password2 + ].each do |field| + expect(partner).to have_field(field) + end + end + + it 'has all the right record refs' do + [ + :klass, :access_role, :department, :subsidiary + ].each do |record_ref| + expect(partner).to have_record_ref(record_ref) + end + end + + describe '.get' do + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :account_number => 7 }) } + + it 'returns an Partner instance populated with the data from the response object' do + expect(NetSuite::Actions::Get).to receive(:call).with([NetSuite::Records::Partner, { :external_id => 1 }], {}).and_return(response) + Partner = NetSuite::Records::Partner.get(:external_id => 1) + expect(Partner).to be_kind_of(NetSuite::Records::Partner) + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'raises a RecordNotFound exception' do + expect(NetSuite::Actions::Get).to receive(:call).with([NetSuite::Records::Partner, { :external_id => 1 }], {}).and_return(response) + expect { + NetSuite::Records::Partner.get(:external_id => 1) + }.to raise_error(NetSuite::RecordNotFound, + /NetSuite::Records::Partner with OPTIONS=(.*) could not be found/) + end + end + end + + describe '#add' do + let(:partner) { NetSuite::Records::Partner.new(:email => 'dale.cooper@example.com') } + + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) } + + it 'returns true' do + expect(NetSuite::Actions::Add).to receive(:call). + with([partner], {}). + and_return(response) + expect(partner.add).to be_truthy + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'returns false' do + expect(NetSuite::Actions::Add).to receive(:call). + with([partner], {}). + and_return(response) + expect(partner.add).to be_falsey + end + end + end + + describe '#delete' do + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) } + + it 'returns true' do + expect(NetSuite::Actions::Delete).to receive(:call). + with([partner], {}). + and_return(response) + expect(partner.delete).to be_truthy + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'returns false' do + expect(NetSuite::Actions::Delete).to receive(:call). + with([partner], {}). + and_return(response) + expect(partner.delete).to be_falsey + end + end + end + + describe '.update' do + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :email => 'leland.palmer@example.com' }) } + + it 'returns true' do + expect(NetSuite::Actions::Update).to receive(:call).with([NetSuite::Records::Partner, { :internal_id => 1, :email => 'leland.palmer@example.com' }], {}).and_return(response) + partner = NetSuite::Records::Partner.new(:internal_id => 1) + expect(partner.update(:email => 'leland.palmer@example.com')).to be_truthy + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'raises a RecordNotFound exception' do + expect(NetSuite::Actions::Update).to receive(:call).with([NetSuite::Records::Partner, { :internal_id => 1, :account_number => 7 }], {}).and_return(response) + partner = NetSuite::Records::Partner.new(:internal_id => 1) + expect(partner.update(:account_number => 7)).to be_falsey + end + end + end + + describe '#to_record' do + let(:partner) { NetSuite::Records::Partner.new(:email => 'bob@example.com') } + + it 'returns a hash of attributes that can be used in a SOAP request' do + expect(partner.to_record).to eql({ 'listRel:email' => 'bob@example.com' }) + end + end + + describe '#record_type' do + it 'returns a string type for the record to be used in a SOAP request' do + expect(partner.record_type).to eql('listRel:Partner') + end + end + + it 'has the right record_refs' do + [ + :klass, :access_role, :department, :subsidiary + ].each do |record_ref| + expect(partner).to have_record_ref(record_ref) + end + end +end diff --git a/spec/netsuite/records/service_resale_item_spec.rb b/spec/netsuite/records/service_resale_item_spec.rb new file mode 100644 index 000000000..b59f99176 --- /dev/null +++ b/spec/netsuite/records/service_resale_item_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe NetSuite::Records::ServiceResaleItem do + let(:item) { NetSuite::Records::ServiceResaleItem.new } + + it 'has the right fields' do + [ + :available_to_partners, :cost, :cost_estimate, :cost_estimate_type, :cost_estimate_units, :create_job, :created_date, + :display_name, :dont_show_price, :enforce_min_qty_internally, :exclude_from_sitemap, :featured_description, + :include_children, :is_donation_item, :is_fulfillable, :is_gco_compliant, :is_inactive, :is_online, :is_taxable, + :item_id, :last_modified_date, :matrix_option_list, :matrix_type, :max_donation_amount, :meta_tag_html, + :minimum_quantity, :minimum_quantity_units, :no_price_message, :offer_support, :on_special, :out_of_stock_behavior, + :out_of_stock_message, :overall_quantity_pricing_type, :page_title, :presentation_item_list, :prices_include_tax, + :show_default_donation_amount, :site_category_list, :sitemap_priority, :soft_descriptor, :specials_description, + :store_description, :store_detailed_description, :store_display_name, :translations_list, :upc_code, :url_component, + :use_marginal_rates, :vsoe_deferral, :vsoe_delivered, :vsoe_permit_discount, :vsoe_price, :vsoe_sop_group + ].each do |field| + expect(item).to have_field(field) + end + + # TODO there is a probably a more robust way to test this + expect(item.custom_field_list.class).to eq(NetSuite::Records::CustomFieldList) + expect(item.pricing_matrix.class).to eq(NetSuite::Records::PricingMatrix) + expect(item.subsidiary_list.class).to eq(NetSuite::Records::RecordRefList) + end + + it 'has the right record_refs' do + [ + :billing_schedule, :cost_category, :custom_form, :deferred_revenue_account, :department, :income_account, + :issue_product, :item_options_list, :klass, :location, :parent, :pricing_group, :purchase_tax_code, + :quantity_pricing_schedule, :rev_rec_schedule, :sale_unit, :sales_tax_code, :store_display_image, + :store_display_thumbnail, :store_item_template, :tax_schedule, :units_type + ].each do |record_ref| + expect(item).to have_record_ref(record_ref) + end + end + + describe '.get' do + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :item_id => 'penguins' }) } + + it 'returns a ServiceResaleItem instance populated with the data from the response object' do + expect(NetSuite::Actions::Get).to receive(:call).with([NetSuite::Records::ServiceResaleItem, :external_id => 20], {}).and_return(response) + customer = NetSuite::Records::ServiceResaleItem.get(:external_id => 20) + expect(customer).to be_kind_of(NetSuite::Records::ServiceResaleItem) + expect(customer.item_id).to eql('penguins') + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'raises a RecordNotFound exception' do + expect(NetSuite::Actions::Get).to receive(:call).with([NetSuite::Records::ServiceResaleItem, :external_id => 20], {}).and_return(response) + expect { + NetSuite::Records::ServiceResaleItem.get(:external_id => 20) + }.to raise_error(NetSuite::RecordNotFound, + /NetSuite::Records::ServiceResaleItem with OPTIONS=(.*) could not be found/) + end + end + end + + describe '#add' do + let(:item) { NetSuite::Records::ServiceResaleItem.new(:cost => 100, :is_inactive => false) } + + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) } + + it 'returns true' do + expect(NetSuite::Actions::Add).to receive(:call). + with([item], {}). + and_return(response) + expect(item.add).to be_truthy + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'returns false' do + expect(NetSuite::Actions::Add).to receive(:call). + with([item], {}). + and_return(response) + expect(item.add).to be_falsey + end + end + end + + describe '#delete' do + context 'when the response is successful' do + let(:response) { NetSuite::Response.new(:success => true, :body => { :internal_id => '1' }) } + + it 'returns true' do + expect(NetSuite::Actions::Delete).to receive(:call). + with([item], {}). + and_return(response) + expect(item.delete).to be_truthy + end + end + + context 'when the response is unsuccessful' do + let(:response) { NetSuite::Response.new(:success => false, :body => {}) } + + it 'returns false' do + expect(NetSuite::Actions::Delete).to receive(:call). + with([item], {}). + and_return(response) + expect(item.delete).to be_falsey + end + end + end + + describe '#to_record' do + before do + item.item_id = 'penguins' + item.is_online = true + end + + it 'can represent itself as a SOAP record' do + record = { + 'listAcct:itemId' => 'penguins', + 'listAcct:isOnline' => true + } + expect(item.to_record).to eql(record) + end + end + + describe '#record_type' do + it 'returns a string of the SOAP type' do + expect(item.record_type).to eql('listAcct:ServiceResaleItem') + end + end + +end diff --git a/spec/netsuite/records/support_case_type_spec.rb b/spec/netsuite/records/support_case_type_spec.rb new file mode 100644 index 000000000..0259e52c7 --- /dev/null +++ b/spec/netsuite/records/support_case_type_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe NetSuite::Records::SupportCaseType do + let(:support_case_type) { NetSuite::Records::SupportCaseType.new } + + it 'has all the right fields' do + [ + :description, :is_inactive, :name + ].each do |field| + expect(support_case_type).to have_field(field) + end + end + + it 'has the right record_refs' do + [ + :insert_before + ].each do |record_ref| + expect(support_case_type).to have_record_ref(record_ref) + end + end + +end diff --git a/spec/netsuite/support/search_result_spec.rb b/spec/netsuite/support/search_result_spec.rb new file mode 100644 index 000000000..53256e58c --- /dev/null +++ b/spec/netsuite/support/search_result_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe NetSuite::Support::SearchResult do + describe '#results' do + context 'empty page' do + it 'returns empty array' do + response_body = { + :status => {:@is_success=>"true"}, + :total_records => "242258", + :page_size => "10", + :total_pages => "24226", + :page_index => "99", + :search_id => "WEBSERVICES_4132604_SB1_051620191060155623420663266_336cbf12", + :record_list => nil, + :"@xmlns:platform_core" => "urn:core_2016_2.platform.webservices.netsuite.com" + } + response = NetSuite::Response.new(body: response_body) + + results = described_class.new(response, NetSuite::Actions::Search, {}).results + expect(results).to eq [] + end + end + end +end