249 lines
12 KiB
Python
249 lines
12 KiB
Python
"""Staff-only upload, preview, and confirmation for customer query order data."""
|
|
|
|
import csv
|
|
import io
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
|
|
from openpyxl import Workbook
|
|
|
|
from logistics_chat import create_app, db, order_data
|
|
from tests.staff_fixture import login_staff
|
|
|
|
|
|
HEADERS = ['租户ID', '订单号', '业务状态', '入仓码', 'Shipment ID']
|
|
|
|
|
|
def csv_upload(rows):
|
|
stream = io.StringIO(newline='')
|
|
writer = csv.writer(stream)
|
|
writer.writerow(HEADERS)
|
|
writer.writerows(rows)
|
|
return (io.BytesIO(stream.getvalue().encode('utf-8-sig')), 'orders.csv')
|
|
|
|
|
|
class OrderImportAdminTests(unittest.TestCase):
|
|
def setUp(self):
|
|
folder = tempfile.TemporaryDirectory()
|
|
self.addCleanup(folder.cleanup)
|
|
root = Path(folder.name)
|
|
self.app = create_app({'TESTING': True, 'DATABASE': root / 'history.db',
|
|
'UPLOAD_DIR': root / 'uploads', 'BACKUP_DIR': root / 'backups',
|
|
'LOG_DIR': root / 'logs', 'DEEPSEEK_API_KEY': '',
|
|
'CUSTOMER_DEMO_ENABLED': False, 'RATE_LIMIT_PER_MIN': 1000})
|
|
self.client = self.app.test_client()
|
|
with self.app.app_context():
|
|
connection = db.connect()
|
|
try:
|
|
connection.execute('INSERT INTO customer_users(username,password_hash,display_name,tenant_id) '
|
|
"VALUES('tenant-a-user','hash','Tenant A','tenant-a')")
|
|
connection.commit()
|
|
finally:
|
|
connection.close()
|
|
|
|
def signed_in(self):
|
|
login_staff(self.app, self.client)
|
|
return self.client.environ_base['HTTP_X_CSRF_TOKEN']
|
|
|
|
def add_existing(self):
|
|
with self.app.app_context():
|
|
connection = db.connect()
|
|
try:
|
|
with connection:
|
|
order_data.import_records(connection, [{'tenant_id': 'tenant-a',
|
|
'order_no': 'EXISTING001', 'status': 'PROCESSING', 'inbound_code': 'OLDINB001'}])
|
|
finally:
|
|
connection.close()
|
|
|
|
def preview(self, upload, csrf=None):
|
|
headers = {'X-CSRF-Token': csrf} if csrf is not None else {}
|
|
return self.client.post('/api/admin/orders/import/preview',
|
|
data={'file': upload}, headers=headers, content_type='multipart/form-data')
|
|
|
|
def test_import_page_and_template_are_staff_protected(self):
|
|
self.assertEqual(self.client.get('/admin/orders').status_code, 302)
|
|
self.signed_in()
|
|
page = self.client.get('/admin/orders')
|
|
self.assertEqual(page.status_code, 200)
|
|
self.assertIn('订单数据管理', page.get_data(as_text=True))
|
|
template = self.client.get('/api/admin/orders/import/template')
|
|
self.assertEqual(template.status_code, 200)
|
|
self.assertIn('租户ID,订单号', template.get_data(as_text=True))
|
|
|
|
def test_preview_validates_rows_and_reports_existing_orders_without_writing(self):
|
|
self.signed_in()
|
|
self.add_existing()
|
|
response = self.preview(csv_upload([
|
|
['tenant-a', 'NEW000001', 'PROCESSING', 'INB000001', 'SHIP000001'],
|
|
['tenant-a', 'EXISTING001', 'COMPLETED', 'NEWINB001', ''],
|
|
]), self.client.environ_base['HTTP_X_CSRF_TOKEN'])
|
|
self.assertEqual(response.status_code, 200, response.get_data(as_text=True))
|
|
self.assertEqual(response.json['summary'], {'total_rows': 2, 'valid_rows': 2,
|
|
'duplicate_rows': 1, 'error_rows': 0})
|
|
self.assertEqual(response.json['preview'][0]['order_no'], 'NEW000001')
|
|
with self.app.app_context():
|
|
connection = db.connect()
|
|
try:
|
|
self.assertEqual(connection.execute('SELECT COUNT(*) FROM order_test_data').fetchone()[0], 1)
|
|
finally:
|
|
connection.close()
|
|
|
|
def test_invalid_file_does_not_partially_import_rows(self):
|
|
self.signed_in()
|
|
response = self.preview(csv_upload([
|
|
['tenant-a', 'NEW000001', 'PROCESSING', 'INB000001', ''],
|
|
['', 'BROKEN0001', 'PROCESSING', '', ''],
|
|
]), self.client.environ_base['HTTP_X_CSRF_TOKEN'])
|
|
self.assertEqual(response.status_code, 400)
|
|
self.assertEqual(response.json['errors'][0]['row'], 3)
|
|
with self.app.app_context():
|
|
connection = db.connect()
|
|
try:
|
|
self.assertEqual(connection.execute('SELECT COUNT(*) FROM order_test_data').fetchone()[0], 0)
|
|
finally:
|
|
connection.close()
|
|
|
|
def test_confirm_import_requires_csrf_and_preserves_existing_order_values(self):
|
|
csrf = self.signed_in()
|
|
self.add_existing()
|
|
upload = csv_upload([
|
|
['tenant-a', 'NEW000001', 'PROCESSING', 'INB000001', 'SHIP000001'],
|
|
['tenant-a', 'EXISTING001', 'COMPLETED', 'NEWINB001', ''],
|
|
])
|
|
payload = upload[0].getvalue()
|
|
blocked = self.client.post('/api/admin/orders/import',
|
|
data={'file': (io.BytesIO(payload), 'orders.csv')}, headers={'X-CSRF-Token': ''},
|
|
content_type='multipart/form-data')
|
|
self.assertEqual(blocked.status_code, 403)
|
|
response = self.client.post('/api/admin/orders/import',
|
|
data={'file': (io.BytesIO(payload), 'orders.csv')}, headers={'X-CSRF-Token': csrf},
|
|
content_type='multipart/form-data')
|
|
self.assertEqual(response.status_code, 200, response.get_data(as_text=True))
|
|
self.assertEqual(response.json['imported'], 1)
|
|
self.assertEqual(response.json['preserved'], 1)
|
|
with self.app.app_context():
|
|
connection = db.connect()
|
|
try:
|
|
self.assertEqual(connection.execute('SELECT status,inbound_code FROM order_test_data '
|
|
'WHERE order_no="EXISTING001"').fetchone(), ('PROCESSING', 'OLDINB001'))
|
|
self.assertEqual(connection.execute('SELECT tenant_id FROM order_test_data '
|
|
'WHERE order_no="NEW000001"').fetchone(), ('tenant-a',))
|
|
finally:
|
|
connection.close()
|
|
|
|
def test_xlsx_workbook_can_be_previewed(self):
|
|
csrf = self.signed_in()
|
|
workbook = Workbook()
|
|
sheet = workbook.active
|
|
sheet.append(HEADERS)
|
|
sheet.append(['tenant-a', 'XLSX000001', 'SUBMITTED', 'XLSXINB01', ''])
|
|
content = io.BytesIO()
|
|
workbook.save(content)
|
|
response = self.preview((io.BytesIO(content.getvalue()), 'orders.xlsx'), csrf)
|
|
self.assertEqual(response.status_code, 200, response.get_data(as_text=True))
|
|
self.assertEqual(response.json['preview'][0]['order_no'], 'XLSX000001')
|
|
|
|
def test_preview_rejects_unknown_tenant_status_and_malformed_tracking(self):
|
|
csrf = self.signed_in()
|
|
response = self.preview(csv_upload([
|
|
['unknown-tenant', 'ORDER000001', 'PROCESSING', '', ''],
|
|
['tenant-a', 'ORDER000002', 'NOT_A_STATUS', '', ''],
|
|
]), csrf)
|
|
self.assertEqual(response.status_code, 400)
|
|
self.assertEqual(response.json['summary']['error_rows'], 2)
|
|
|
|
stream = io.StringIO(newline='')
|
|
writer = csv.writer(stream)
|
|
writer.writerow(['租户ID', '订单号', '业务状态', '物流轨迹(JSON)'])
|
|
writer.writerow(['tenant-a', 'ORDER000003', 'PROCESSING', '[{"code":"NOT_A_NODE","actual_at":"2026-11-01"}]'])
|
|
invalid_tracking = self.preview((io.BytesIO(stream.getvalue().encode('utf-8-sig')), 'orders.csv'), csrf)
|
|
self.assertEqual(invalid_tracking.status_code, 400)
|
|
self.assertIn('物流轨迹', invalid_tracking.json['errors'][0]['message'])
|
|
|
|
def test_import_records_batch_and_undo_only_deletes_unchanged_inserted_rows(self):
|
|
csrf = self.signed_in()
|
|
self.add_existing()
|
|
response = self.client.post('/api/admin/orders/import',
|
|
data={'file': csv_upload([
|
|
['tenant-a', 'NEW000001', 'PROCESSING', 'INB000001', 'SHIP000001'],
|
|
['tenant-a', 'NEW000002', 'PROCESSING', '', ''],
|
|
['tenant-a', 'EXISTING001', 'COMPLETED', '', ''],
|
|
])}, headers={'X-CSRF-Token': csrf}, content_type='multipart/form-data')
|
|
self.assertEqual(response.status_code, 200, response.get_data(as_text=True))
|
|
self.assertEqual(response.json['imported'], 2)
|
|
self.assertEqual(response.json['preserved'], 1)
|
|
batch_id = response.json['batch_id']
|
|
self.assertGreater(batch_id, 0)
|
|
|
|
listed = self.client.get('/api/admin/orders/list?q=NEW000001')
|
|
self.assertEqual(listed.status_code, 200)
|
|
self.assertEqual(listed.json['total'], 1)
|
|
self.assertEqual(listed.json['orders'][0]['order_no'], 'NEW000001')
|
|
filtered = self.client.get('/api/admin/orders/list?tenant=tenant-a&status=PROCESSING&q=%25')
|
|
self.assertEqual(filtered.status_code, 200)
|
|
self.assertEqual(filtered.json['total'], 0)
|
|
|
|
with self.app.app_context():
|
|
connection = db.connect()
|
|
try:
|
|
connection.execute("UPDATE order_test_data SET data_note='人工备注' WHERE order_no='NEW000002'")
|
|
connection.commit()
|
|
finally:
|
|
connection.close()
|
|
undone = self.client.post(f'/api/admin/orders/import/batches/{batch_id}/undo',
|
|
headers={'X-CSRF-Token': csrf})
|
|
self.assertEqual(undone.status_code, 200, undone.get_data(as_text=True))
|
|
self.assertEqual(undone.json['deleted'], 1)
|
|
self.assertEqual(undone.json['preserved'], 1)
|
|
repeated = self.client.post(f'/api/admin/orders/import/batches/{batch_id}/undo',
|
|
headers={'X-CSRF-Token': csrf})
|
|
self.assertEqual(repeated.status_code, 200)
|
|
self.assertTrue(repeated.json['already_undone'])
|
|
with self.app.app_context():
|
|
connection = db.connect()
|
|
try:
|
|
self.assertIsNone(connection.execute("SELECT 1 FROM order_test_data "
|
|
"WHERE order_no='NEW000001'").fetchone())
|
|
self.assertEqual(connection.execute("SELECT data_note FROM order_test_data "
|
|
"WHERE order_no='NEW000002'").fetchone()[0], '人工备注')
|
|
self.assertEqual(connection.execute("SELECT status FROM order_test_data "
|
|
"WHERE order_no='EXISTING001'").fetchone()[0], 'PROCESSING')
|
|
finally:
|
|
connection.close()
|
|
|
|
def test_order_listing_requires_staff_and_csrf_for_undo(self):
|
|
self.assertEqual(self.client.get('/api/admin/orders/list').status_code, 401)
|
|
csrf = self.signed_in()
|
|
response = self.client.post('/api/admin/orders/import',
|
|
data={'file': csv_upload([['tenant-a', 'NEW000002', 'PROCESSING', '', '']])},
|
|
headers={'X-CSRF-Token': csrf}, content_type='multipart/form-data')
|
|
batch_id = response.json['batch_id']
|
|
denied = self.client.post(f'/api/admin/orders/import/batches/{batch_id}/undo',
|
|
headers={'X-CSRF-Token': ''})
|
|
self.assertEqual(denied.status_code, 403)
|
|
|
|
def test_schema_upgrade_adds_batch_tables_without_losing_orders(self):
|
|
self.add_existing()
|
|
with self.app.app_context():
|
|
connection = db.connect()
|
|
try:
|
|
connection.execute("UPDATE schema_meta SET value='3' WHERE key='schema_version'")
|
|
connection.execute('DROP TABLE order_import_batch_items')
|
|
connection.execute('DROP TABLE order_import_batches')
|
|
connection.commit()
|
|
finally:
|
|
connection.close()
|
|
db.init_db()
|
|
connection = db.connect()
|
|
try:
|
|
self.assertEqual(connection.execute("SELECT value FROM schema_meta WHERE key='schema_version'").fetchone()[0], '4')
|
|
self.assertEqual(connection.execute("SELECT status FROM order_test_data WHERE order_no='EXISTING001'").fetchone()[0], 'PROCESSING')
|
|
self.assertIsNotNone(connection.execute("SELECT 1 FROM sqlite_master WHERE name='order_import_batches'").fetchone())
|
|
finally:
|
|
connection.close()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|