-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: NetSuite Internal APIs #658
Changes from all commits
4bf6487
2e3d6a9
67f3622
9e549bb
50183d3
7d3dd36
dc25863
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,30 @@ | ||||||||||||||||||||||||||||||||||||||||||
from typing import Dict | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
from apps.netsuite.connector import NetSuiteConnector | ||||||||||||||||||||||||||||||||||||||||||
from apps.workspaces.models import Workspace, NetSuiteCredentials | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
def get_netsuite_connection(query_params: Dict): | ||||||||||||||||||||||||||||||||||||||||||
org_id = query_params.get('org_id') | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
workspace = Workspace.objects.get(fyle_org_id=org_id) | ||||||||||||||||||||||||||||||||||||||||||
workspace_id = workspace.id | ||||||||||||||||||||||||||||||||||||||||||
ns_credentials = NetSuiteCredentials.objects.get(workspace_id=workspace.id) | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return NetSuiteConnector(netsuite_credentials=ns_credentials, workspace_id=workspace_id) | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
def get_accounting_fields(query_params: Dict): | ||||||||||||||||||||||||||||||||||||||||||
ns_connection = get_netsuite_connection(query_params) | ||||||||||||||||||||||||||||||||||||||||||
resource_type = query_params.get('resource_type') | ||||||||||||||||||||||||||||||||||||||||||
internal_id = query_params.get('internal_id') | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return ns_connection.get_accounting_fields(resource_type, internal_id) | ||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+17
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add parameter validation and error handling The function needs validation for required parameters and proper error handling. Implement these improvements: def get_accounting_fields(query_params: Dict):
+ required_params = ['resource_type', 'internal_id']
+ for param in required_params:
+ if not query_params.get(param):
+ raise ValueError(f'{param} is required in query parameters')
+
ns_connection = get_netsuite_connection(query_params)
resource_type = query_params.get('resource_type')
internal_id = query_params.get('internal_id')
- return ns_connection.get_accounting_fields(resource_type, internal_id)
+ try:
+ return ns_connection.get_accounting_fields(resource_type, internal_id)
+ except Exception as e:
+ raise ValueError(f'Failed to get accounting fields: {str(e)}') 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
def get_exported_entry(query_params: Dict): | ||||||||||||||||||||||||||||||||||||||||||
ns_connection = get_netsuite_connection(query_params) | ||||||||||||||||||||||||||||||||||||||||||
resource_type = query_params.get('resource_type') | ||||||||||||||||||||||||||||||||||||||||||
internal_id = query_params.get('internal_id') | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return ns_connection.get_exported_entry(resource_type, internal_id) | ||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+25
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Refactor to reduce code duplication and add validation This function is nearly identical to Here's a suggested implementation: +def _validate_and_get_params(query_params: Dict, operation: str):
+ required_params = ['resource_type', 'internal_id']
+ for param in required_params:
+ if not query_params.get(param):
+ raise ValueError(f'{param} is required in query parameters')
+
+ ns_connection = get_netsuite_connection(query_params)
+ resource_type = query_params.get('resource_type')
+ internal_id = query_params.get('internal_id')
+ return ns_connection, resource_type, internal_id
+
def get_exported_entry(query_params: Dict):
- ns_connection = get_netsuite_connection(query_params)
- resource_type = query_params.get('resource_type')
- internal_id = query_params.get('internal_id')
+ ns_connection, resource_type, internal_id = _validate_and_get_params(
+ query_params, 'exported_entry'
+ )
- return ns_connection.get_exported_entry(resource_type, internal_id)
+ try:
+ return ns_connection.get_exported_entry(resource_type, internal_id)
+ except Exception as e:
+ raise ValueError(f'Failed to get exported entry: {str(e)}') Also update def get_accounting_fields(query_params: Dict):
- ns_connection = get_netsuite_connection(query_params)
- resource_type = query_params.get('resource_type')
- internal_id = query_params.get('internal_id')
+ ns_connection, resource_type, internal_id = _validate_and_get_params(
+ query_params, 'accounting_fields'
+ )
- return ns_connection.get_accounting_fields(resource_type, internal_id)
+ try:
+ return ns_connection.get_accounting_fields(resource_type, internal_id)
+ except Exception as e:
+ raise ValueError(f'Failed to get accounting fields: {str(e)}')
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class InternalConfig(AppConfig): | ||
default_auto_field = 'django.db.models.BigAutoField' | ||
name = 'apps.internal' |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,10 @@ | ||||
import itertools | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is not needed |
||||
|
||||
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yo, drop that unused import like it's hot! Listen up! That Here's the fix, straight from the streets of good coding practices: -import itertools 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff
|
||||
from django.urls import path | ||||
|
||||
from .views import AccountingFieldsView, ExportedEntryView | ||||
|
||||
urlpatterns = [ | ||||
path('accounting_fields/', AccountingFieldsView.as_view(), name='accounting-fields'), | ||||
path('exported_entry/', ExportedEntryView.as_view(), name='exported-entry'), | ||||
] |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,68 @@ | ||||||
import logging | ||||||
import traceback | ||||||
from rest_framework import generics | ||||||
from rest_framework.response import Response | ||||||
from rest_framework import status | ||||||
|
||||||
from apps.workspaces.permissions import IsAuthenticatedForInternalAPI | ||||||
|
||||||
from fyle_netsuite_api.utils import assert_valid | ||||||
|
||||||
from .actions import get_accounting_fields, get_exported_entry | ||||||
|
||||||
logger = logging.getLogger(__name__) | ||||||
logger.setLevel(logging.INFO) | ||||||
|
||||||
|
||||||
class AccountingFieldsView(generics.GenericAPIView): | ||||||
authentication_classes = [] | ||||||
permission_classes = [IsAuthenticatedForInternalAPI] | ||||||
Comment on lines
+18
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix contradictory authentication configuration The empty class AccountingFieldsView(generics.GenericAPIView):
- authentication_classes = []
+ authentication_classes = [TokenAuthentication] # or appropriate auth class
permission_classes = [IsAuthenticatedForInternalAPI]
|
||||||
|
||||||
def get(self, request, *args, **kwargs): | ||||||
try: | ||||||
params = request.query_params | ||||||
|
||||||
assert_valid(params.get('org_id') is not None, 'Org ID is required') | ||||||
assert_valid(params.get('resource_type') is not None, 'Resource Type is required') | ||||||
|
||||||
if params.get('resource_type') in ('custom_segments', 'custom_lists', 'custom_record_types'): | ||||||
assert_valid(params.get('internal_id') is not None, 'Internal ID is required') | ||||||
|
||||||
response = get_accounting_fields(request.query_params) | ||||||
return Response( | ||||||
data={'data': response}, | ||||||
status=status.HTTP_200_OK | ||||||
) | ||||||
|
||||||
except Exception: | ||||||
logger.info(f"Error in AccountingFieldsView: {traceback.format_exc()}") | ||||||
return Response( | ||||||
data={'error': traceback.format_exc()}, | ||||||
status=status.HTTP_400_BAD_REQUEST | ||||||
) | ||||||
|
||||||
|
||||||
class ExportedEntryView(generics.GenericAPIView): | ||||||
authentication_classes = [] | ||||||
permission_classes = [IsAuthenticatedForInternalAPI] | ||||||
|
||||||
def get(self, request, *args, **kwargs): | ||||||
try: | ||||||
params = request.query_params | ||||||
assert_valid(params.get('org_id') is not None, 'Org ID is required') | ||||||
assert_valid(params.get('resource_type') is not None, 'Resource Type is required') | ||||||
assert_valid(params.get('internal_id') is not None, 'Internal ID is required') | ||||||
|
||||||
Comment on lines
+51
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add resource type validation and refactor validation logic
+VALID_RESOURCE_TYPES = {'custom_segments', 'custom_lists', 'custom_record_types', ...} # add all valid types
+
+def validate_params(params, require_internal_id=True):
+ assert_valid(params.get('org_id') is not None, 'Org ID is required')
+ assert_valid(params.get('resource_type') is not None, 'Resource Type is required')
+ assert_valid(
+ params.get('resource_type') in VALID_RESOURCE_TYPES,
+ f'Invalid resource type. Must be one of: {", ".join(VALID_RESOURCE_TYPES)}'
+ )
+ if require_internal_id:
+ assert_valid(params.get('internal_id') is not None, 'Internal ID is required')
+
def get(self, request, *args, **kwargs):
try:
params = request.query_params
- assert_valid(params.get('org_id') is not None, 'Org ID is required')
- assert_valid(params.get('resource_type') is not None, 'Resource Type is required')
- assert_valid(params.get('internal_id') is not None, 'Internal ID is required')
+ validate_params(params)
|
||||||
response = get_exported_entry(request.query_params) | ||||||
|
||||||
return Response( | ||||||
data={'data': response}, | ||||||
status=status.HTTP_200_OK | ||||||
) | ||||||
|
||||||
except Exception: | ||||||
logger.info(f"Error in AccountingFieldsView: {traceback.format_exc()}") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix incorrect logger message The error message incorrectly references "AccountingFieldsView" instead of "ExportedEntryView". - logger.info(f"Error in AccountingFieldsView: {traceback.format_exc()}")
+ logger.info(f"Error in ExportedEntryView: {traceback.format_exc()}") 📝 Committable suggestion
Suggested change
|
||||||
return Response( | ||||||
data={'error': traceback.format_exc()}, | ||||||
status=status.HTTP_400_BAD_REQUEST | ||||||
) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -1162,7 +1162,42 @@ def sync_customers(self): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
attributes, 'PROJECT', self.workspace_id, True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def get_accounting_fields(self, resource_type: str, internal_id: str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Retrieve accounting fields for a specific resource type and internal ID. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
resource_type (str): The type of resource to fetch. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
internal_id (str): The internal ID of the resource. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
list or dict: Parsed JSON representation of the resource data. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
module = getattr(self.connection, resource_type) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
method_map = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
'currencies': 'get_all', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ruuushhh marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
'custom_segments': 'get', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
'custom_lists': 'get', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
'custom_record_types': 'get_all_by_id', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
method = method_map.get(resource_type, 'get_all_generator') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if method in ('get', 'get_all_by_id'): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
response = getattr(module, method)(internal_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
response = getattr(module, method)() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if method == 'get_all_generator': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
response = [row for responses in response for row in responses] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return json.loads(json.dumps(response, default=str)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+1166
to
+1194
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling and improve type safety. The method needs several improvements for robustness and type safety:
Here's the suggested implementation: - def get_accounting_fields(self, resource_type: str, internal_id: str):
+ def get_accounting_fields(self, resource_type: str, internal_id: str) -> Union[List, Dict]:
+ """
+ Retrieve accounting fields for a specific resource type and internal ID.
+
+ Args:
+ resource_type (str): The type of resource to fetch.
+ internal_id (str): The internal ID of the resource.
+
+ Returns:
+ Union[List, Dict]: Parsed JSON representation of the resource data.
+
+ Raises:
+ ValueError: If resource_type is invalid or internal_id is empty.
+ """
+ if not resource_type:
+ raise ValueError('Resource type is required')
+ if not internal_id:
+ raise ValueError('Internal ID is required')
+
module = getattr(self.connection, resource_type)
method_map = {
'currencies': 'get_all',
'custom_segments': 'get',
'custom_lists': 'get',
'custom_record_types': 'get_all_by_id',
}
- method = method_map.get(resource_type, 'get_all_generator')
+ method_name = method_map.get(resource_type, 'get_all_generator')
+ method = getattr(module, method_name)
- if method in ('get', 'get_all_by_id'):
- response = getattr(module, method)(internal_id)
+ if method_name in ('get', 'get_all_by_id'):
+ response = method(internal_id)
else:
- response = getattr(module, method)()
+ response = method()
- if method == 'get_all_generator':
+ if method_name == 'get_all_generator':
response = [row for responses in response for row in responses]
return json.loads(json.dumps(response, default=str)) 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def get_exported_entry(self, resource_type: str, export_id: str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
module = getattr(self.connection, resource_type) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
response = getattr(module, 'get')(export_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return json.loads(json.dumps(response, default=str)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+1196
to
+1199
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling and improve type safety. The method needs several improvements for robustness and type safety:
Here's the suggested implementation: - def get_exported_entry(self, resource_type: str, export_id: str):
+ def get_exported_entry(self, resource_type: str, export_id: str) -> Dict:
+ """
+ Retrieve a specific exported entry.
+
+ Args:
+ resource_type (str): The type of resource to fetch.
+ export_id (str): The export ID of the entry.
+
+ Returns:
+ Dict: Parsed JSON representation of the exported entry.
+
+ Raises:
+ ValueError: If resource_type is invalid or export_id is empty.
+ """
+ if not resource_type:
+ raise ValueError('Resource type is required')
+ if not export_id:
+ raise ValueError('Export ID is required')
+
module = getattr(self.connection, resource_type)
- response = getattr(module, 'get')(export_id)
+ response = module.get(export_id)
return json.loads(json.dumps(response, default=str))
🧰 Tools🪛 Ruff (0.8.0)1198-1198: Do not call Replace (B009) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def handle_taxed_line_items(self, base_line, line, workspace_id, export_module, general_mapping: GeneralMapping): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Handle line items where tax is applied or modified by the user. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,15 +34,15 @@ def has_permission(self, request, view): | |
workspace_users = Workspace.objects.filter(pk=workspace_id).values_list('user', flat=True) | ||
return self.validate_and_cache(workspace_users, user, workspace_id, True) | ||
|
||
class IsAuthenticatedForTest(permissions.BasePermission): | ||
class IsAuthenticatedForInternalAPI(permissions.BasePermission): | ||
""" | ||
Custom auth for preparing a workspace for e2e tests | ||
Custom auth for internal APIs | ||
""" | ||
def has_permission(self, request, view): | ||
# Client sends a token in the header, which we decrypt and compare with the Client Secret | ||
cipher_suite = Fernet(settings.ENCRYPTION_KEY) | ||
try: | ||
decrypted_password = cipher_suite.decrypt(request.headers['X-E2E-Tests-Client-ID'].encode('utf-8')).decode('utf-8') | ||
decrypted_password = cipher_suite.decrypt(request.headers['X-Internal-API-Client-ID'].encode('utf-8')).decode('utf-8') | ||
if decrypted_password == settings.E2E_TESTS_CLIENT_SECRET: | ||
return True | ||
Comment on lines
+45
to
47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance security measures The current implementation could be strengthened with additional security measures. Consider these improvements:
Example implementation: from django.core.cache import cache
from django.utils import timezone
from rest_framework.exceptions import Throttled
class IsAuthenticatedForInternalAPI(permissions.BasePermission):
def has_permission(self, request, view):
# Rate limiting
cache_key = f"internal_api_auth_{request.META.get('REMOTE_ADDR')}"
attempts = cache.get(cache_key, 0)
if attempts >= 5: # 5 attempts per minute
raise Throttled()
cache.set(cache_key, attempts + 1, 60) # 60 seconds expiry
# IP allowlist check
client_ip = request.META.get('REMOTE_ADDR')
if client_ip not in settings.INTERNAL_API_ALLOWED_IPS:
logger.warning(f'Unauthorized IP attempt: {client_ip}')
return False
# Existing authentication logic
cipher_suite = Fernet(settings.ENCRYPTION_KEY)
try:
decrypted_password = cipher_suite.decrypt(
request.headers['X-Internal-API-Client-ID'].encode('utf-8')
).decode('utf-8')
if decrypted_password == settings.E2E_TESTS_CLIENT_SECRET:
return True |
||
except: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,6 +62,7 @@ | |
'apps.netsuite', | ||
'django_q', | ||
'django_filters', | ||
'apps.internal' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Codebase verification Yo, we got a security situation that needs attention! Listen up! Just spotted something that's making me lose myself in the security concerns. That AccountingFieldsView is running wild with no authentication or permissions - that's like leaving the door wide open at 8 Mile!
authentication_classes = []
permission_classes = []
Better lock this down before someone takes advantage, know what I'm saying?
Don't let this internal app be your security's kryptonite! 🔗 Analysis chainYo, let's verify this internal app integration! Listen up! Before we drop this new app into production, we gotta make sure it's legit. Here's what's on my mind: Straight from the underground, here's what you need to know:
Yo, and one more thing - consider adding some middleware specifically for this internal app if you're exposing sensitive operations. Keep it secure, keep it clean! 🏁 Scripts executedThe following scripts were executed for the analysis: Script: #!/bin/bash
# Spit some facts about this internal app structure
echo "Checking if this internal app is properly structured..."
fd -t f "^__init__.py|^apps.py|^models.py|^views.py|^urls.py$" "apps/internal"
# Look for any sensitive stuff that might be exposed
echo "Scanning for potential security gotchas..."
rg -g "apps/internal/**" -i "password|secret|key|token|credential"
Length of output: 816 Script: #!/bin/bash
# Yo, let's check what security we got in these views
echo "Dropping bars about view security..."
rg -g "apps/internal/views.py" -A 5 "class|def"
# Check if we're using any authentication decorators
echo "Checking authentication flow..."
rg -g "apps/internal/views.py" "@.*_required|permission_required|login_required"
# Let's see what URLs we're exposing
echo "Scanning URL patterns..."
rg -g "apps/internal/urls.py" "path|url"
# Check if we got any middleware specifically for this
echo "Looking for custom middleware..."
rg -g "apps/internal/**" "class.*Middleware"
Length of output: 1288 |
||
] | ||
|
||
MIDDLEWARE = [ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,4 +22,5 @@ | |
path('api/workspaces/', include('apps.workspaces.urls')), | ||
path('api/user/', include('apps.users.urls')), | ||
path('api/v2/workspaces/', include('apps.workspaces.apis.urls')), | ||
path('api/internal/', include('apps.internal.urls')), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Codebase verification Yo, this endpoint's more exposed than 8 Mile Road! Security's MIA! Listen up, I'm dropping truth bombs here:
You better fix this before it goes south:
This ain't a freestyle session where anything goes - lock it down! 🔗 Analysis chainYo, hold up! Let's talk about securing these internal APIs! Listen up, we're exposing internal endpoints here, and that's some serious business. We better make sure these routes are locked down tight with proper authentication and authorization. Let me drop these bars to check for any security middleware: 🏁 Scripts executedThe following scripts were executed for the analysis: Script: #!/bin/bash
# Yo, let's check if we got security measures in place
echo "Checking for authentication middleware..."
rg -l "AuthenticationMiddleware|permission_classes|authentication_classes" apps/internal/
echo "Looking for security decorators..."
ast-grep --pattern 'class $_ {
$$$
@permission_required($$$)
$$$
}'
Length of output: 362 Script: #!/bin/bash
# Yo, let's dive deeper into these security measures
echo "Checking the actual security implementation in views..."
rg -A 5 "permission_classes|authentication_classes" apps/internal/views.py
echo "\nLooking for any middleware configuration..."
rg -l "MIDDLEWARE" fyle_netsuite_api/settings/
echo "\nChecking what's in those middleware settings..."
for f in $(fd -t f . fyle_netsuite_api/settings/); do
echo "\nFile: $f"
rg "MIDDLEWARE.*=.*\[" -A 10 "$f"
done
echo "\nLet's see what's in those internal views..."
cat apps/internal/views.py
Length of output: 1918 |
||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
from apps.internal.actions import get_accounting_fields, get_exported_entry | ||
from tests.test_netsuite.fixtures import data | ||
|
||
def test_get_accounting_fields(db, mocker): | ||
query_params = { | ||
'org_id': 'or79Cob97KSh', | ||
'resource_type': 'employees', | ||
} | ||
mocker.patch( | ||
'netsuitesdk.api.employees.Employees.get_all_generator', | ||
return_value=data['get_all_employees'] | ||
) | ||
|
||
mocker.patch('netsuitesdk.api.currencies.Currencies.get_all') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix empty mock for Currencies.get_all The mock for Currencies.get_all is missing a return value, which could lead to unexpected behavior. - mocker.patch('netsuitesdk.api.currencies.Currencies.get_all')
+ mocker.patch(
+ 'netsuitesdk.api.currencies.Currencies.get_all',
+ return_value=data['get_all_currencies']
+ )
|
||
|
||
mocker.patch( | ||
'netsuitesdk.api.custom_lists.CustomLists.get', | ||
return_value=data['get_custom_list'] | ||
) | ||
|
||
fields = get_accounting_fields(query_params) | ||
assert fields is not None | ||
|
||
query_params['resource_type'] = 'custom_lists' | ||
query_params['internal_id'] = '1' | ||
fields = get_accounting_fields(query_params) | ||
assert fields is not None | ||
|
||
Comment on lines
+4
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add docstring and improve test assertions The test function would benefit from:
def test_get_accounting_fields(db, mocker):
+ """
+ Test get_accounting_fields action with different resource types:
+ 1. Test with employees resource type
+ 2. Test with custom_lists resource type
+ """
query_params = {
- 'org_id': 'or79Cob97KSh',
+ 'org_id': pytest.fixture('org_id'),
'resource_type': 'employees',
}
|
||
|
||
def test_get_exported_entry(db, mocker): | ||
query_params = { | ||
'org_id': 'or79Cob97KSh', | ||
'resource_type': 'vendor_bills', | ||
'internal_id': '1' | ||
} | ||
mocker.patch( | ||
'netsuitesdk.api.vendor_bills.VendorBills.get', | ||
return_value={'summa': 'hehe'} | ||
) | ||
|
||
entry = get_exported_entry(query_params) | ||
assert entry is not None | ||
Comment on lines
+30
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve test data and assertions Several improvements needed:
Here's how to improve it: def test_get_exported_entry(db, mocker):
+ """
+ Test get_exported_entry action retrieves correct vendor bill data
+ """
query_params = {
- 'org_id': 'or79Cob97KSh',
- 'resource_type': 'vendor_bills',
- 'internal_id': '1'
+ 'org_id': pytest.fixture('org_id'),
+ 'resource_type': 'vendor_bills',
+ 'internal_id': pytest.fixture('vendor_bill_id')
}
+ expected_vendor_bill = {
+ 'internalId': '1',
+ 'tranId': 'BILL#123',
+ 'amount': 100.00,
+ 'status': 'PENDING'
+ }
mocker.patch(
'netsuitesdk.api.vendor_bills.VendorBills.get',
- return_value={'summa': 'hehe'}
+ return_value=expected_vendor_bill
)
entry = get_exported_entry(query_params)
- assert entry is not None
+ assert entry == expected_vendor_bill
+ assert 'internalId' in entry
+ assert 'tranId' in entry
+ assert 'amount' in entry
+ assert 'status' in entry
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,61 @@ | ||||||||||||||||||||||||||||||
import pytest | ||||||||||||||||||||||||||||||
from unittest.mock import patch | ||||||||||||||||||||||||||||||
from django.urls import reverse | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
from apps.workspaces.permissions import IsAuthenticatedForInternalAPI | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
from tests.test_netsuite.fixtures import data | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@pytest.mark.django_db(databases=['default']) | ||||||||||||||||||||||||||||||
@patch.object(IsAuthenticatedForInternalAPI, 'has_permission', return_value=True) | ||||||||||||||||||||||||||||||
def test_netsutie_fields_view(db, api_client, mocker): | ||||||||||||||||||||||||||||||
url = reverse('accounting-fields') | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
response = api_client.get(url) | ||||||||||||||||||||||||||||||
assert response.status_code == 400 | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
response = api_client.get(url, {'org_id': 'or79Cob97KSh'}) | ||||||||||||||||||||||||||||||
assert response.status_code == 400 | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
response = api_client.get(url, {'org_id': 'or79Cob97KSh', 'resource_type': 'custom_segments'}) | ||||||||||||||||||||||||||||||
assert response.status_code == 400 | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
mocker.patch( | ||||||||||||||||||||||||||||||
'netsuitesdk.api.custom_lists.CustomLists.get', | ||||||||||||||||||||||||||||||
return_value=data['get_custom_list'] | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
response = api_client.get(url, {'org_id': 'or79Cob97KSh', 'resource_type': 'custom_lists', 'internal_id': '1'}) | ||||||||||||||||||||||||||||||
assert response.status_code == 200 | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
mocker.patch( | ||||||||||||||||||||||||||||||
'netsuitesdk.api.employees.Employees.get_all_generator', | ||||||||||||||||||||||||||||||
return_value=data['get_all_employees'] | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
response = api_client.get(url, {'org_id': 'or79Cob97KSh', 'resource_type': 'employees'}) | ||||||||||||||||||||||||||||||
assert response.status_code == 200 | ||||||||||||||||||||||||||||||
Comment on lines
+10
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance test coverage with additional assertions The test could be more comprehensive:
Consider adding: # Test invalid resource type
response = api_client.get(url, {
'org_id': TEST_ORG_ID,
'resource_type': 'invalid_type'
})
assert response.status_code == 400
# Verify response structure
response = api_client.get(url, {
'org_id': TEST_ORG_ID,
'resource_type': 'employees'
})
assert response.status_code == 200
assert 'data' in response.json()
assert isinstance(response.json()['data'], list)
# Test SDK error handling
mocker.patch(
'netsuitesdk.api.employees.Employees.get_all_generator',
side_effect=Exception('NetSuite API error')
)
response = api_client.get(url, {
'org_id': TEST_ORG_ID,
'resource_type': 'employees'
})
assert response.status_code == 500 |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@pytest.mark.django_db(databases=['default']) | ||||||||||||||||||||||||||||||
@patch.object(IsAuthenticatedForInternalAPI, 'has_permission', return_value=True) | ||||||||||||||||||||||||||||||
def test_exported_entry_view(db, api_client, mocker): | ||||||||||||||||||||||||||||||
url = reverse('exported-entry') | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
response = api_client.get(url) | ||||||||||||||||||||||||||||||
assert response.status_code == 400 | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
response = api_client.get(url, {'org_id': 'or79Cob97KSh'}) | ||||||||||||||||||||||||||||||
assert response.status_code == 400 | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
response = api_client.get(url, {'org_id': 'or79Cob97KSh', 'resource_type': 'vendor_bills'}) | ||||||||||||||||||||||||||||||
assert response.status_code == 400 | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
mocker.patch( | ||||||||||||||||||||||||||||||
'netsuitesdk.api.vendor_bills.VendorBills.get', | ||||||||||||||||||||||||||||||
return_value={'summa': 'hehe'} | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
Comment on lines
+55
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use representative test data The mock return value - return_value={'summa': 'hehe'}
+ return_value={
+ 'internalId': '1',
+ 'tranId': 'BILL123',
+ 'total': 100.00,
+ 'status': 'PENDING_APPROVAL',
+ 'memo': 'Test Bill'
+ } 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
response = api_client.get(url, {'org_id': 'or79Cob97KSh', 'resource_type': 'vendor_bills', 'internal_id': '1'}) | ||||||||||||||||||||||||||||||
assert response.status_code == 200 | ||||||||||||||||||||||||||||||
Comment on lines
+41
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance test coverage for exported entry view Similar to the previous test, this one could benefit from:
Consider adding: # Test different resource types
for resource_type in ['vendor_bills', 'expense_reports', 'journal_entries']:
mocker.patch(
f'netsuitesdk.api.{resource_type}.{resource_type.title().replace("_", "")}.get',
return_value=data[f'get_{resource_type}']
)
response = api_client.get(url, {
'org_id': TEST_ORG_ID,
'resource_type': resource_type,
'internal_id': '1'
})
assert response.status_code == 200
assert 'data' in response.json()
# Test SDK error handling
mocker.patch(
'netsuitesdk.api.vendor_bills.VendorBills.get',
side_effect=Exception('NetSuite API error')
)
response = api_client.get(url, {
'org_id': TEST_ORG_ID,
'resource_type': 'vendor_bills',
'internal_id': '1'
})
assert response.status_code == 500 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add input validation and error handling
The function has several critical issues that need to be addressed:
org_id
parameterConsider implementing these improvements:
📝 Committable suggestion