Skip to content

Commit 951297b

Browse files
committed
Separate transaction fields into individual database columns
This update separates the transaction description into distinct fields for better data integrity and querying: - Added merchant, original_statement, notes, and tags as separate database columns - Updated CSV import to store fields individually instead of concatenating them - Enhanced frontend edit modal to support all transaction fields - Improved error reporting in CSV import to show first 50 errors with categorized summary - Updated all utility scripts to work from scripts/ directory with proper path handling - Added database migration scripts for new columns This change enables better filtering, searching, and reporting on transaction data by maintaining field separation throughout the system.
1 parent 0e64598 commit 951297b

File tree

10 files changed

+345
-23
lines changed

10 files changed

+345
-23
lines changed

api/transaction/controllers.py

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
'transaction_type': fields.String(required=True, description='Transaction Type'),
2323
'external_id': fields.String(description='External ID'),
2424
'external_date': fields.DateTime(description='External Date'),
25+
'merchant': fields.String(description='Merchant Name'),
26+
'original_statement': fields.String(description='Original Statement'),
27+
'notes': fields.String(description='Notes'),
28+
'tags': fields.String(description='Tags'),
2529
'description': fields.String(description='Description')
2630
})
2731

@@ -37,6 +41,10 @@ def post(self):
3741
transaction_type = data.get('transaction_type')
3842
external_id = data.get('external_id')
3943
external_date = data.get('external_date')
44+
merchant = data.get('merchant')
45+
original_statement = data.get('original_statement')
46+
notes = data.get('notes')
47+
tags = data.get('tags')
4048
description = data.get('description')
4149

4250
# Validate required fields
@@ -61,6 +69,10 @@ def post(self):
6169
transaction_type=transaction_type,
6270
external_id=external_id,
6371
external_date=external_date,
72+
merchant=merchant,
73+
original_statement=original_statement,
74+
notes=notes,
75+
tags=tags,
6476
description=description
6577
)
6678
new_transaction.save()
@@ -210,20 +222,11 @@ def post(self):
210222
error_count += 1
211223
continue
212224

213-
# Build description from available fields
214-
description_parts = []
215-
if merchant:
216-
description_parts.append(f"Merchant: {merchant}")
217-
if original_statement:
218-
description_parts.append(f"Statement: {original_statement}")
219-
if notes:
220-
description_parts.append(f"Notes: {notes}")
221-
if tags:
222-
description_parts.append(f"Tags: {tags}")
225+
# Store fields separately (no joining)
226+
# Description can be used for additional custom info if needed
227+
description = None # Keep empty unless specifically provided
223228

224-
description = " | ".join(description_parts) if description_parts else merchant
225-
226-
# Create transaction
229+
# Create transaction with all fields separate
227230
self.create_transaction(
228231
user_id=user_id,
229232
categories_id=category_id,
@@ -232,6 +235,10 @@ def post(self):
232235
transaction_type=_transaction_type,
233236
external_id=external_id,
234237
external_date=formatted_timestamp,
238+
merchant=merchant,
239+
original_statement=original_statement,
240+
notes=notes,
241+
tags=tags,
235242
description=description
236243
)
237244
created_count += 1
@@ -251,8 +258,24 @@ def post(self):
251258
'errors': error_count
252259
}
253260

254-
if errors and len(errors) <= 10: # Only include first 10 errors
255-
response_data['error_details'] = errors[:10]
261+
if errors:
262+
# Show first 50 errors to understand patterns
263+
response_data['error_details'] = errors[:50]
264+
# Also include a summary of error types
265+
error_summary = {}
266+
for error in errors:
267+
# Extract error type
268+
if "Category" in error and "not found" in error:
269+
error_summary['category_not_found'] = error_summary.get('category_not_found', 0) + 1
270+
elif "Could not create account" in error:
271+
error_summary['account_creation_failed'] = error_summary.get('account_creation_failed', 0) + 1
272+
elif "Could not parse" in error:
273+
error_summary['date_parse_error'] = error_summary.get('date_parse_error', 0) + 1
274+
elif "Invalid amount" in error:
275+
error_summary['invalid_amount'] = error_summary.get('invalid_amount', 0) + 1
276+
else:
277+
error_summary['other'] = error_summary.get('other', 0) + 1
278+
response_data['error_summary'] = error_summary
256279

257280
return make_response(jsonify(response_data), 201 if created_count > 0 else 200)
258281

@@ -347,7 +370,7 @@ def ensure_account_exists_smart(self, account_name, merchant_name):
347370

348371
return account.id
349372

350-
def create_transaction(self,user_id, categories_id, account_id, amount, transaction_type, external_id, external_date, description):
373+
def create_transaction(self,user_id, categories_id, account_id, amount, transaction_type, external_id, external_date, merchant=None, original_statement=None, notes=None, tags=None, description=None):
351374
print(f"Creating transaction {external_id}...")
352375
transaction = TransactionModel(
353376
user_id=user_id,
@@ -357,6 +380,10 @@ def create_transaction(self,user_id, categories_id, account_id, amount, transact
357380
transaction_type=transaction_type,
358381
external_id=external_id,
359382
external_date=external_date,
383+
merchant=merchant,
384+
original_statement=original_statement,
385+
notes=notes,
386+
tags=tags,
360387
description=description
361388
)
362389
db.session.add(transaction)
@@ -408,6 +435,14 @@ def put(self, id):
408435
}), 400)
409436

410437
# Update fields if provided
438+
if 'merchant' in data:
439+
transaction.merchant = data['merchant']
440+
if 'original_statement' in data:
441+
transaction.original_statement = data['original_statement']
442+
if 'notes' in data:
443+
transaction.notes = data['notes']
444+
if 'tags' in data:
445+
transaction.tags = data['tags']
411446
if 'description' in data:
412447
transaction.description = data['description']
413448
if 'amount' in data:

api/transaction/models.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ class TransactionModel(Base):
2525
transaction_type = db.Column(db.String(255), nullable=False)
2626
external_id = db.Column(db.String(255), nullable=False)
2727
external_date = db.Column(db.DateTime, nullable=True)
28+
merchant = db.Column(db.String(255), nullable=True)
29+
original_statement = db.Column(db.String(255), nullable=True)
30+
notes = db.Column(db.Text, nullable=True)
31+
tags = db.Column(db.String(255), nullable=True)
2832
description = db.Column(db.String(255), nullable=True)
2933

30-
def __init__(self, user_id, categories_id, account_id, amount, transaction_type, external_id, external_date, description):
34+
def __init__(self, user_id, categories_id, account_id, amount, transaction_type, external_id, external_date, merchant=None, original_statement=None, notes=None, tags=None, description=None):
3135
"""
3236
Initialize a TransactionModel instance.
3337
@@ -39,6 +43,10 @@ def __init__(self, user_id, categories_id, account_id, amount, transaction_type,
3943
transaction_type (str): The transaction_type of the transaction.
4044
external_id (str): The external ID of the transaction.
4145
external_date (datetime): The external date of the transaction.
46+
merchant (str): The merchant name for the transaction.
47+
original_statement (str): The original statement text from the bank.
48+
notes (str): User notes for the transaction.
49+
tags (str): Tags for categorizing the transaction.
4250
description (str): The description of the transaction.
4351
"""
4452
self.user_id = user_id
@@ -48,6 +56,10 @@ def __init__(self, user_id, categories_id, account_id, amount, transaction_type,
4856
self.transaction_type = transaction_type
4957
self.external_id = external_id
5058
self.external_date = external_date
59+
self.merchant = merchant
60+
self.original_statement = original_statement
61+
self.notes = notes
62+
self.tags = tags
5163
self.description = description
5264

5365
def __repr__(self):
@@ -77,6 +89,10 @@ def to_dict(self):
7789
'transaction_type': self.transaction_type,
7890
'external_id': self.external_id,
7991
'external_date': self.external_date,
92+
'merchant': self.merchant,
93+
'original_statement': self.original_statement,
94+
'notes': self.notes,
95+
'tags': self.tags,
8096
'description': self.description,
8197
'created_at': self.created_at,
8298
'updated_at': self.updated_at

app/static/js/transactions/import.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,30 @@ uploadBtn.addEventListener('click', async () => {
117117
function showSuccess(data) {
118118
let errorDetailsHtml = '';
119119
if (data.error_details && data.error_details.length > 0) {
120+
let errorSummaryHtml = '';
121+
if (data.error_summary) {
122+
errorSummaryHtml = `
123+
<div class="mb-3">
124+
<strong>Error Summary:</strong>
125+
<ul>
126+
${Object.entries(data.error_summary).map(([type, count]) =>
127+
`<li>${type.replace(/_/g, ' ')}: ${count}</li>`
128+
).join('')}
129+
</ul>
130+
</div>
131+
`;
132+
}
133+
120134
errorDetailsHtml = `
121135
<hr>
122136
<div class="alert alert-warning">
123-
<h6 class="alert-heading">Import Errors (showing first 10):</h6>
124-
<ul class="mb-0">
125-
${data.error_details.map(err => `<li>${err}</li>`).join('')}
126-
</ul>
137+
<h6 class="alert-heading">Import Errors (showing first 50):</h6>
138+
${errorSummaryHtml}
139+
<div style="max-height: 300px; overflow-y: auto;">
140+
<ul class="mb-0">
141+
${data.error_details.map(err => `<li><small>${err}</small></li>`).join('')}
142+
</ul>
143+
</div>
127144
</div>
128145
`;
129146
}

app/static/js/transactions/transactions.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ async function editTransaction(transactionId) {
8888
if (response.ok) {
8989
// Populate the edit form
9090
document.getElementById('edit_transaction_id').value = transactionId;
91+
document.getElementById('edit_transactionMerchant').value = data.transaction.merchant || '';
92+
document.getElementById('edit_transactionOriginalStatement').value = data.transaction.original_statement || '';
93+
document.getElementById('edit_transactionNotes').value = data.transaction.notes || '';
94+
document.getElementById('edit_transactionTags').value = data.transaction.tags || '';
9195
document.getElementById('edit_transactionDescription').value = data.transaction.description || '';
9296
document.getElementById('edit_transactionAmount').value = data.transaction.amount || '';
9397
document.getElementById('edit_transactionCategory').value = data.transaction.categories_id || '';
@@ -109,13 +113,21 @@ function updateTransaction(event) {
109113
event.preventDefault();
110114

111115
const transactionId = document.getElementById('edit_transaction_id').value;
116+
const merchant = document.getElementById('edit_transactionMerchant').value;
117+
const originalStatement = document.getElementById('edit_transactionOriginalStatement').value;
118+
const notes = document.getElementById('edit_transactionNotes').value;
119+
const tags = document.getElementById('edit_transactionTags').value;
112120
const description = document.getElementById('edit_transactionDescription').value;
113121
const amount = document.getElementById('edit_transactionAmount').value;
114122
const categoryId = document.getElementById('edit_transactionCategory').value;
115123
const accountId = document.getElementById('edit_transactionAccount').value;
116124
const user_id = document.getElementById('edit_transaction_user_id').value;
117125

118126
const data = {
127+
merchant: merchant,
128+
original_statement: originalStatement,
129+
notes: notes,
130+
tags: tags,
119131
description: description,
120132
amount: parseFloat(amount),
121133
categories_id: categoryId,

app/templates/transactions/index.html

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ <h5 class="modal-title" id="TransactionsModalgridLabel">Add
107107
</div>
108108
</th>
109109
<th scope="col">Date</th>
110+
<th scope="col">Merchant</th>
110111
<th scope="col">Description</th>
111112
<th scope="col">Category</th>
112113
<th scope="col">Amount</th>
@@ -127,9 +128,10 @@ <h5 class="modal-title" id="TransactionsModalgridLabel">Add
127128
</div>
128129
</td>
129130
<td>{{ transaction.external_date|format_datetime('short') }}</td>
131+
<td>{{ transaction.merchant }}</td>
130132
<td>{{ transaction.description }}</td>
131-
<td>{{ transaction.amount }}</td>
132133
<td>{{ transaction.categories.name }}</td>
134+
<td>{{ transaction.amount }}</td>
133135
<td>{{ transaction.account.name }}</td>
134136
<td>{{ transaction.account.institution.name }}</td>
135137
<td>
@@ -162,10 +164,34 @@ <h5 class="modal-title" id="EditTransactionModalLabel">Edit Transaction</h5>
162164
<input type="hidden" id="edit_transaction_id">
163165
<input type="hidden" id="edit_transaction_user_id" value="{{ user_id }}">
164166
<div class="row g-3">
167+
<div class="col-xxl-12">
168+
<div>
169+
<label for="edit_transactionMerchant" class="form-label">Merchant</label>
170+
<input type="text" class="form-control" id="edit_transactionMerchant" placeholder="Enter Merchant Name">
171+
</div>
172+
</div>
173+
<div class="col-xxl-12">
174+
<div>
175+
<label for="edit_transactionOriginalStatement" class="form-label">Original Statement</label>
176+
<input type="text" class="form-control" id="edit_transactionOriginalStatement" placeholder="Original Bank Statement">
177+
</div>
178+
</div>
179+
<div class="col-xxl-12">
180+
<div>
181+
<label for="edit_transactionNotes" class="form-label">Notes</label>
182+
<textarea class="form-control" id="edit_transactionNotes" rows="2" placeholder="Enter Notes"></textarea>
183+
</div>
184+
</div>
185+
<div class="col-xxl-12">
186+
<div>
187+
<label for="edit_transactionTags" class="form-label">Tags</label>
188+
<input type="text" class="form-control" id="edit_transactionTags" placeholder="Enter Tags (comma separated)">
189+
</div>
190+
</div>
165191
<div class="col-xxl-12">
166192
<div>
167193
<label for="edit_transactionDescription" class="form-label">Description</label>
168-
<input type="text" class="form-control" id="edit_transactionDescription" placeholder="Enter Transaction Description">
194+
<input type="text" class="form-control" id="edit_transactionDescription" placeholder="Additional Description">
169195
</div>
170196
</div>
171197
<div class="col-xxl-12">

scripts/add_merchant_column.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Migration script to add merchant column to transaction table
4+
"""
5+
import os
6+
import sys
7+
8+
# Change to project root directory (parent of scripts directory)
9+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10+
os.chdir(project_root)
11+
sys.path.insert(0, project_root)
12+
13+
from app import create_app, db
14+
from sqlalchemy import text
15+
16+
app = create_app()
17+
18+
with app.app_context():
19+
try:
20+
# Add the merchant column if it doesn't exist
21+
db.session.execute(text("""
22+
ALTER TABLE transaction
23+
ADD COLUMN IF NOT EXISTS merchant VARCHAR(255);
24+
"""))
25+
db.session.commit()
26+
print("✓ Successfully added merchant column to transaction table")
27+
except Exception as e:
28+
print(f"✗ Error adding merchant column: {e}")
29+
db.session.rollback()

scripts/add_transaction_fields.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Migration script to add original_statement, notes, and tags columns to transaction table
4+
"""
5+
import os
6+
import sys
7+
8+
# Change to project root directory (parent of scripts directory)
9+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10+
os.chdir(project_root)
11+
sys.path.insert(0, project_root)
12+
13+
from app import create_app, db
14+
from sqlalchemy import text
15+
16+
app = create_app()
17+
18+
with app.app_context():
19+
try:
20+
# Add the new columns if they don't exist
21+
db.session.execute(text("""
22+
ALTER TABLE transaction
23+
ADD COLUMN IF NOT EXISTS original_statement VARCHAR(255);
24+
"""))
25+
db.session.execute(text("""
26+
ALTER TABLE transaction
27+
ADD COLUMN IF NOT EXISTS notes TEXT;
28+
"""))
29+
db.session.execute(text("""
30+
ALTER TABLE transaction
31+
ADD COLUMN IF NOT EXISTS tags VARCHAR(255);
32+
"""))
33+
db.session.commit()
34+
print("✓ Successfully added original_statement, notes, and tags columns to transaction table")
35+
except Exception as e:
36+
print(f"✗ Error adding columns: {e}")
37+
db.session.rollback()

0 commit comments

Comments
 (0)